Browse Source

Correction of windows' digest authentication, new database migration to fit the needs and adjusting tracim's login/change password/user creation to calculate and insert the new hash

Nonolost 8 years ago
parent
commit
297c5b3ecd

+ 22 - 0
tracim/migration/versions/b4b8d57b54e5_add_hash_column_for_digest_.py View File

1
+"""add hash column for digest authentication
2
+
3
+Revision ID: b4b8d57b54e5
4
+Revises: 534c4594ed29
5
+Create Date: 2016-08-11 10:27:28.951506
6
+
7
+"""
8
+
9
+# revision identifiers, used by Alembic.
10
+revision = 'b4b8d57b54e5'
11
+down_revision = '534c4594ed29'
12
+
13
+from alembic import op
14
+from sqlalchemy import Column, Unicode
15
+
16
+
17
+def upgrade():
18
+    op.add_column('users', Column('webdav_left_digest_response_hash', Unicode(128)))
19
+
20
+
21
+def downgrade():
22
+    op.drop_column('users', 'webdav_left_digest_response_hash')

+ 3 - 0
tracim/tracim/controllers/admin/user.py View File

328
             # Setup a random password to send email at user
328
             # Setup a random password to send email at user
329
             password = str(uuid.uuid4())
329
             password = str(uuid.uuid4())
330
             user.password = password
330
             user.password = password
331
+
332
+        user.webdav_left_digest_response_hash = '%s:/:%s' % (email, password)
333
+
331
         api.save(user)
334
         api.save(user)
332
 
335
 
333
         # Now add the user to related groups
336
         # Now add the user to related groups

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

123
             tg.redirect(redirect_url)
123
             tg.redirect(redirect_url)
124
 
124
 
125
         current_user.password = new_password1
125
         current_user.password = new_password1
126
+        current_user.webdav_left_digest_response_hash = '%s:/:%s' % (current_user.email, new_password1)
126
         pm.DBSession.flush()
127
         pm.DBSession.flush()
127
 
128
 
128
         tg.flash(_('Your password has been changed'))
129
         tg.flash(_('Your password has been changed'))

+ 7 - 0
tracim/tracim/lib/auth/internal.py View File

5
 from tracim.lib.auth.base import Auth
5
 from tracim.lib.auth.base import Auth
6
 from tracim.model import DBSession, User
6
 from tracim.model import DBSession, User
7
 
7
 
8
+# TODO : temporary fix to update DB, to remove
9
+import transaction
8
 
10
 
9
 class InternalAuth(Auth):
11
 class InternalAuth(Auth):
10
 
12
 
34
         )).first()
36
         )).first()
35
 
37
 
36
         if user and user.validate_password(identity['password']):
38
         if user and user.validate_password(identity['password']):
39
+            if 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()
37
             return identity['login']
44
             return identity['login']
38
 
45
 
39
     def get_user(self, identity, userid):
46
     def get_user(self, identity, userid):

+ 22 - 1
tracim/tracim/lib/daemons.py View File

239
 DEFAULT_CONFIG_FILE = "wsgidav.conf"
239
 DEFAULT_CONFIG_FILE = "wsgidav.conf"
240
 PYTHON_VERSION = "%s.%s.%s" % (sys.version_info[0], sys.version_info[1], sys.version_info[2])
240
 PYTHON_VERSION = "%s.%s.%s" % (sys.version_info[0], sys.version_info[1], sys.version_info[2])
241
 
241
 
242
+def _get_checked_path(path, mustExist=True, allowNone=True):
243
+    """Convert path to absolute if not None."""
244
+    if path in (None, ""):
245
+        if allowNone:
246
+            return None
247
+        else:
248
+            raise ValueError("Invalid path %r" % path)
249
+    path = os.path.abspath(path)
250
+    if mustExist and not os.path.exists(path):
251
+        raise ValueError("Invalid path %r" % path)
252
+    return path
253
+
242
 class WsgiDavDaemon(Daemon):
254
 class WsgiDavDaemon(Daemon):
243
 
255
 
244
     def __init__(self, *args, **kwargs):
256
     def __init__(self, *args, **kwargs):
261
         if not useLxml and config["verbose"] >= 1:
273
         if not useLxml and config["verbose"] >= 1:
262
             print(
274
             print(
263
                 "WARNING: Could not import lxml: using xml instead (slower). Consider installing lxml from http://codespeak.net/lxml/.")
275
                 "WARNING: Could not import lxml: using xml instead (slower). Consider installing lxml from http://codespeak.net/lxml/.")
276
+        from wsgidav.dir_browser import WsgiDavDirBrowser
277
+        from wsgidav.debug_filter import WsgiDavDebugFilter
278
+        from tracim.lib.webdav.tracim_http_authenticator import TracimHTTPAuthenticator
279
+        from wsgidav.error_printer import ErrorPrinter
280
+
281
+        config['middleware_stack'] = [ WsgiDavDirBrowser, TracimHTTPAuthenticator, ErrorPrinter, WsgiDavDebugFilter ]
264
 
282
 
265
         config['provider_mapping'] = \
283
         config['provider_mapping'] = \
266
             {
284
             {
273
             }
291
             }
274
 
292
 
275
         config['domaincontroller'] = SQLDomainController(presetdomain=None, presetserver=None)
293
         config['domaincontroller'] = SQLDomainController(presetdomain=None, presetserver=None)
276
-        config['defaultdigest'] = False
294
+        config['defaultdigest'] = True
295
+        config['acceptdigest'] = True
296
+
277
         return config
297
         return config
278
 
298
 
279
     def _readConfigFile(self, config_file, verbose):
299
     def _readConfigFile(self, config_file, verbose):
323
                 PYTHON_VERSION)
343
                 PYTHON_VERSION)
324
 
344
 
325
             wsgiserver.CherryPyWSGIServer.version = version
345
             wsgiserver.CherryPyWSGIServer.version = version
346
+
326
             protocol = "http"
347
             protocol = "http"
327
 
348
 
328
             if config["verbose"] >= 1:
349
             if config["verbose"] >= 1:

+ 10 - 10
tracim/tracim/lib/webdav/__init__.py View File

6
 from wsgidav import util
6
 from wsgidav import util
7
 
7
 
8
 import transaction
8
 import transaction
9
+from wsgidav import compat
9
 
10
 
10
 class HistoryType(object):
11
 class HistoryType(object):
11
     Deleted = 'deleted'
12
     Deleted = 'deleted'
27
         :param workspace: content's workspace, necessary if the file is new as we've got no other way to get it
28
         :param workspace: content's workspace, necessary if the file is new as we've got no other way to get it
28
         :param content: either the content to be updated or None if it's a new file
29
         :param content: either the content to be updated or None if it's a new file
29
         """
30
         """
30
-        self._buffer = []
31
+        self._buff = compat.BytesIO()
32
+
31
         self._file_name = file_name if file_name != '' else self._content.file_name
33
         self._file_name = file_name if file_name != '' else self._content.file_name
32
         self._content = content
34
         self._content = content
33
         self._api = content_api
35
         self._api = content_api
44
 
46
 
45
     def write(self, s: str):
47
     def write(self, s: str):
46
         """Called by request_server when writing content to files, we stock it in our file"""
48
         """Called by request_server when writing content to files, we stock it in our file"""
47
-        self._buffer.append(s)
49
+        self._buff.write(s)
48
 
50
 
49
     def close(self):
51
     def close(self):
50
         """Called by request_server when everything has been written and we either update the file or
52
         """Called by request_server when everything has been written and we either update the file or
51
         create a new file"""
53
         create a new file"""
52
-        item_content = b''
53
 
54
 
54
-        for part in self._buffer:
55
-            item_content += part
55
+        self._buff.seek(0)
56
 
56
 
57
         if self._content is None:
57
         if self._content is None:
58
-            self.create_file(item_content)
58
+            self.create_file(self._buff)
59
         else:
59
         else:
60
-            self.update_file(item_content)
60
+            self.update_file(self._buff)
61
 
61
 
62
     def create_file(self, item_content):
62
     def create_file(self, item_content):
63
         file = self._api.create(
63
         file = self._api.create(
70
             file,
70
             file,
71
             self._file_name,
71
             self._file_name,
72
             util.guessMimeType(self._file_name),
72
             util.guessMimeType(self._file_name),
73
-            item_content
73
+            item_content.read()
74
         )
74
         )
75
 
75
 
76
         self._api.save(file, ActionDescription.CREATION)
76
         self._api.save(file, ActionDescription.CREATION)
77
 
77
 
78
         transaction.commit()
78
         transaction.commit()
79
 
79
 
80
-    def update_file(self, item_content: bytes):
80
+    def update_file(self, item_content):
81
         with new_revision(self._content):
81
         with new_revision(self._content):
82
             self._api.update_file_data(
82
             self._api.update_file_data(
83
                 self._content,
83
                 self._content,
84
                 self._file_name,
84
                 self._file_name,
85
                 util.guessMimeType(self._content.file_name),
85
                 util.guessMimeType(self._content.file_name),
86
-                item_content
86
+                item_content.read()
87
             )
87
             )
88
             self._api.save(self._content, ActionDescription.EDITION)
88
             self._api.save(self._content, ActionDescription.EDITION)
89
 
89
 

+ 3 - 0
tracim/tracim/lib/webdav/sql_dav_provider.py View File

61
     #########################################################
61
     #########################################################
62
     # Everything override from DAVProvider
62
     # Everything override from DAVProvider
63
     def getResourceInst(self, path, environ):
63
     def getResourceInst(self, path, environ):
64
+        print("ok : ", path)
64
         #if not self.exists(path, environ):
65
         #if not self.exists(path, environ):
65
         #    return None
66
         #    return None
66
         if not self.exists(path, environ):
67
         if not self.exists(path, environ):
178
             return None
179
             return None
179
 
180
 
180
     def exists(self, path, environ):
181
     def exists(self, path, environ):
182
+        print("ok (exist) : ", path)
181
         uapi = UserApi(None)
183
         uapi = UserApi(None)
182
         environ['user'] = uapi.get_one_by_email(environ['http_authenticator.username'])
184
         environ['user'] = uapi.get_one_by_email(environ['http_authenticator.username'])
183
 
185
 
193
         )
195
         )
194
 
196
 
195
         if path == root_path:
197
         if path == root_path:
198
+            print("ok (pass ici) : ", path)
196
             return True
199
             return True
197
         elif parent_path == root_path:
200
         elif parent_path == root_path:
198
             return self.get_workspace_from_path(
201
             return self.get_workspace_from_path(

+ 16 - 9
tracim/tracim/lib/webdav/sql_domain_controller.py View File

8
         self._api = UserApi(None)
8
         self._api = UserApi(None)
9
 
9
 
10
     def getDomainRealm(self, inputURL, environ):
10
     def getDomainRealm(self, inputURL, environ):
11
-        """
12
-        On va récupérer le workspace de travail pour travailler sur les droits
13
-        """
11
+
12
+        '''On va récupérer le workspace de travail pour travailler sur les droits'''
13
+
14
         return '/'
14
         return '/'
15
 
15
 
16
     def requireAuthentication(self, realmname, environ):
16
     def requireAuthentication(self, realmname, environ):
17
         return True
17
         return True
18
 
18
 
19
     def isRealmUser(self, realmname, username, environ):
19
     def isRealmUser(self, realmname, username, environ):
20
-        """
21
-        travailler dans la bdd pour vérifier si utilisateur existe
22
-        """
20
+
21
+        '''travailler dans la bdd pour vérifier si utilisateur existe'''
22
+
23
         try:
23
         try:
24
             self._api.get_one_by_email(username)
24
             self._api.get_one_by_email(username)
25
             return True
25
             return True
27
             return False
27
             return False
28
 
28
 
29
     def getRealmUserPassword(self, realmname, username, environ):
29
     def getRealmUserPassword(self, realmname, username, environ):
30
-        """Retourne le mdp pour l'utilisateur pour ce real"""
30
+        '''Retourne le mdp pour l'utilisateur pour ce real'''
31
         try:
31
         try:
32
             user = self._api.get_one_by_email(username)
32
             user = self._api.get_one_by_email(username)
33
             return user.password
33
             return user.password
34
         except:
34
         except:
35
             return None
35
             return None
36
 
36
 
37
+    def get_left_digest_response_hash(self, realmname, username, environ):
38
+        try:
39
+            user = self._api.get_one_by_email(username)
40
+            return user.webdav_left_digest_response_hash
41
+        except:
42
+            return None
43
+
37
     def authDomainUser(self, realmname, username, password, environ):
44
     def authDomainUser(self, realmname, username, password, environ):
38
-        """Vérifier que l'utilisateur est valide pour ce domaine"""
45
+        '''Vérifier que l'utilisateur est valide pour ce domaine'''
39
         return self.isRealmUser(realmname, username, environ) and \
46
         return self.isRealmUser(realmname, username, environ) and \
40
-            self._api.get_one_by_email(username).validate_password(password)
47
+            self._api.get_one_by_email(username).validate_password(password)

+ 15 - 9
tracim/tracim/lib/webdav/sql_resources.py View File

379
             self.content_api,
379
             self.content_api,
380
             WorkspaceApi(self.environ['user']))
380
             WorkspaceApi(self.environ['user']))
381
 
381
 
382
+        workspace = self.provider.get_workspace_from_path(
383
+            normpath(destpath),
384
+            WorkspaceApi(self.environ['user'])
385
+        )
386
+
382
         with new_revision(self.content):
387
         with new_revision(self.content):
383
             if basename(destpath) != self.content.label:
388
             if basename(destpath) != self.content.label:
384
                 self.content_api.update_content(self.content, basename(destpath), self.content.description)
389
                 self.content_api.update_content(self.content, basename(destpath), self.content.description)
397
                 try:
402
                 try:
398
                     self.content_api.move_recursively(self.content, parent, parent.workspace)
403
                     self.content_api.move_recursively(self.content, parent, parent.workspace)
399
                 except AttributeError:
404
                 except AttributeError:
400
-                    self.content_api.move_recursively(
401
-                        self.content,
402
-                        parent,
403
-                        self.provider.get_workspace_from_path(
404
-                            destpath,
405
-                            WorkspaceApi(self.environ['user'])
406
-                        )
407
-                    )
405
+                    self.content_api.move_recursively(self.content, parent, workspace)
408
 
406
 
409
         transaction.commit()
407
         transaction.commit()
410
 
408
 
818
             WorkspaceApi(self.environ['user'])
816
             WorkspaceApi(self.environ['user'])
819
         )
817
         )
820
 
818
 
819
+        workspace = self.provider.get_workspace_from_path(
820
+            normpath(destpath),
821
+            WorkspaceApi(self.environ['user'])
822
+        )
823
+
821
         with new_revision(self.content):
824
         with new_revision(self.content):
822
             if basename(destpath) != self.content.label:
825
             if basename(destpath) != self.content.label:
823
                 self.content_api.update_content(self.content, basename(destpath), self.content.description)
826
                 self.content_api.update_content(self.content, basename(destpath), self.content.description)
827
                 item=self.content,
830
                 item=self.content,
828
                 new_parent=parent,
831
                 new_parent=parent,
829
                 must_stay_in_same_workspace=False,
832
                 must_stay_in_same_workspace=False,
830
-                new_workspace=parent.workspace
833
+                new_workspace=workspace
831
             )
834
             )
832
 
835
 
833
         transaction.commit()
836
         transaction.commit()
893
     def getDisplayName(self) -> str:
896
     def getDisplayName(self) -> str:
894
         return self.content.get_label()
897
         return self.content.get_label()
895
 
898
 
899
+    def getPreferredPath(self):
900
+        return self.path + '.html'
901
+
896
     def __repr__(self) -> str:
902
     def __repr__(self) -> str:
897
         return "<DAVNonCollection: OtherFile (%s)" % self.content.file_name
903
         return "<DAVNonCollection: OtherFile (%s)" % self.content.file_name
898
 
904
 

+ 144 - 0
tracim/tracim/lib/webdav/tracim_http_authenticator.py View File

1
+from wsgidav.http_authenticator import HTTPAuthenticator
2
+from wsgidav import util
3
+import re
4
+
5
+_logger = util.getModuleLogger(__name__, True)
6
+HOTFIX_WINXP_AcceptRootShareLogin = True
7
+
8
+class TracimHTTPAuthenticator(HTTPAuthenticator):
9
+    def __init__(self, application, config):
10
+        super(TracimHTTPAuthenticator, self).__init__(application, config)
11
+        self._headerfixparser = re.compile(r'([\w]+)=("[^"]*,[^"]*"),')
12
+
13
+    def authDigestAuthRequest(self, environ, start_response):
14
+        realmname = self._domaincontroller.getDomainRealm(environ["PATH_INFO"], environ)
15
+
16
+        isinvalidreq = False
17
+
18
+        authheaderdict = dict([])
19
+        authheaders = environ["HTTP_AUTHORIZATION"] + ","
20
+        if not authheaders.lower().strip().startswith("digest"):
21
+            isinvalidreq = True
22
+            # Hotfix for Windows file manager and OSX Finder:
23
+        # Some clients don't urlencode paths in auth header, so uri value may
24
+        # contain commas, which break the usual regex headerparser. Example:
25
+        # Digest username="user",realm="/",uri="a,b.txt",nc=00000001, ...
26
+        # -> [..., ('uri', '"a'), ('nc', '00000001'), ...]
27
+        # Override any such values with carefully extracted ones.
28
+        authheaderlist = self._headerparser.findall(authheaders)
29
+        authheaderfixlist = self._headerfixparser.findall(authheaders)
30
+        if authheaderfixlist:
31
+            _logger.info("Fixing authheader comma-parsing: extend %s with %s" \
32
+                         % (authheaderlist, authheaderfixlist))
33
+            authheaderlist += authheaderfixlist
34
+        for authheader in authheaderlist:
35
+            authheaderkey = authheader[0]
36
+            authheadervalue = authheader[1].strip().strip("\"")
37
+            authheaderdict[authheaderkey] = authheadervalue
38
+
39
+        _logger.debug("authDigestAuthRequest: %s" % environ["HTTP_AUTHORIZATION"])
40
+        _logger.debug("  -> %s" % authheaderdict)
41
+
42
+        if "username" in authheaderdict:
43
+            req_username = authheaderdict["username"]
44
+            req_username_org = req_username
45
+            # Hotfix for Windows XP:
46
+            #   net use W: http://127.0.0.1/dav /USER:DOMAIN\tester tester
47
+            # will send the name with double backslashes ('DOMAIN\\tester')
48
+            # but send the digest for the simple name ('DOMAIN\tester').
49
+            if r"\\" in req_username:
50
+                req_username = req_username.replace("\\\\", "\\")
51
+                _logger.info("Fixing Windows name with double backslash: '%s' --> '%s'" % (req_username_org, req_username))
52
+
53
+            if not self._domaincontroller.isRealmUser(realmname, req_username, environ):
54
+                isinvalidreq = True
55
+        else:
56
+            isinvalidreq = True
57
+
58
+            # TODO: Chun added this comments, but code was commented out
59
+            # Do not do realm checking - a hotfix for WinXP using some other realm's
60
+            # auth details for this realm - if user/password match
61
+        print(authheaderdict.get("realm"), realmname)
62
+        if 'realm' in authheaderdict:
63
+            if authheaderdict["realm"].upper() != realmname.upper():
64
+                if HOTFIX_WINXP_AcceptRootShareLogin:
65
+                    # Hotfix: also accept '/'
66
+                    if authheaderdict["realm"].upper() != "/":
67
+                        isinvalidreq = True
68
+                else:
69
+                    isinvalidreq = True
70
+
71
+        if "algorithm" in authheaderdict:
72
+            if authheaderdict["algorithm"].upper() != "MD5":
73
+                isinvalidreq = True  # only MD5 supported
74
+
75
+        if "uri" in authheaderdict:
76
+            req_uri = authheaderdict["uri"]
77
+
78
+        if "nonce" in authheaderdict:
79
+            req_nonce = authheaderdict["nonce"]
80
+        else:
81
+            isinvalidreq = True
82
+
83
+        req_hasqop = False
84
+        if "qop" in authheaderdict:
85
+            req_hasqop = True
86
+            req_qop = authheaderdict["qop"]
87
+            if req_qop.lower() != "auth":
88
+                isinvalidreq = True  # only auth supported, auth-int not supported
89
+        else:
90
+            req_qop = None
91
+
92
+        if "cnonce" in authheaderdict:
93
+            req_cnonce = authheaderdict["cnonce"]
94
+        else:
95
+            req_cnonce = None
96
+            if req_hasqop:
97
+                isinvalidreq = True
98
+
99
+        if "nc" in authheaderdict:  # is read but nonce-count checking not implemented
100
+            req_nc = authheaderdict["nc"]
101
+        else:
102
+            req_nc = None
103
+            if req_hasqop:
104
+                isinvalidreq = True
105
+
106
+        if "response" in authheaderdict:
107
+            req_response = authheaderdict["response"]
108
+        else:
109
+            isinvalidreq = True
110
+
111
+        if not isinvalidreq:
112
+            left_digest_response_hash = self._domaincontroller.get_left_digest_response_hash(realmname, req_username, environ)
113
+
114
+            req_method = environ["REQUEST_METHOD"]
115
+
116
+            required_digest = self.tracim_compute_digest_response(left_digest_response_hash, req_method, req_uri, req_nonce,
117
+                                                         req_cnonce, req_qop, req_nc)
118
+
119
+            if required_digest != req_response:
120
+                _logger.warning("computeDigestResponse('%s', '%s', ...): %s != %s" % (
121
+                realmname, req_username, required_digest, req_response))
122
+                isinvalidreq = True
123
+            else:
124
+                _logger.debug("digest succeeded for realm '%s', user '%s'" % (realmname, req_username))
125
+            pass
126
+
127
+        if isinvalidreq:
128
+            _logger.warning("Authentication failed for user '%s', realm '%s'" % (req_username, realmname))
129
+            return self.sendDigestAuthResponse(environ, start_response)
130
+
131
+        environ["http_authenticator.realm"] = realmname
132
+        environ["http_authenticator.username"] = req_username
133
+        return self._application(environ, start_response)
134
+
135
+    def tracim_compute_digest_response(self, left_digest_response_hash, method, uri, nonce, cnonce, qop, nc):
136
+        # A1 : username:realm:password
137
+        A2 = method + ":" + uri
138
+        if qop:
139
+            digestresp = self.md5kd(left_digest_response_hash, nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + self.md5h(A2))
140
+        else:
141
+            digestresp = self.md5kd(left_digest_response_hash, nonce + ":" + self.md5h(A2))
142
+            # print(A1, A2)
143
+        # print(digestresp)
144
+        return digestresp

+ 16 - 0
tracim/tracim/model/auth.py View File

14
 from slugify import slugify
14
 from slugify import slugify
15
 from sqlalchemy.ext.hybrid import hybrid_property
15
 from sqlalchemy.ext.hybrid import hybrid_property
16
 from tg.i18n import lazy_ugettext as l_
16
 from tg.i18n import lazy_ugettext as l_
17
+from hashlib import md5
17
 
18
 
18
 __all__ = ['User', 'Group', 'Permission']
19
 __all__ = ['User', 'Group', 'Permission']
19
 
20
 
121
     created = Column(DateTime, default=datetime.now)
122
     created = Column(DateTime, default=datetime.now)
122
     is_active = Column(Boolean, default=True, nullable=False)
123
     is_active = Column(Boolean, default=True, nullable=False)
123
     imported_from = Column(Unicode(32), nullable=True)
124
     imported_from = Column(Unicode(32), nullable=True)
125
+    _webdav_left_digest_response_hash = Column('webdav_left_digest_response_hash', Unicode(128))
124
 
126
 
125
     @hybrid_property
127
     @hybrid_property
126
     def email_address(self):
128
     def email_address(self):
197
     password = synonym('_password', descriptor=property(_get_password,
199
     password = synonym('_password', descriptor=property(_get_password,
198
                                                         _set_password))
200
                                                         _set_password))
199
 
201
 
202
+    @classmethod
203
+    def _hash_digest(cls, digest):
204
+        return md5(bytes(digest, 'utf-8')).hexdigest()
205
+
206
+    def _set_hash_digest(self, digest):
207
+        self._webdav_left_digest_response_hash = self._hash_digest(digest)
208
+
209
+    def _get_hash_digest(self):
210
+        return self._webdav_left_digest_response_hash
211
+
212
+    webdav_left_digest_response_hash = synonym('_webdav_left_digest_response_hash',
213
+                                               descriptor=property(_get_hash_digest,
214
+                                                                    _set_hash_digest))
215
+
200
     def validate_password(self, password):
216
     def validate_password(self, password):
201
         """
217
         """
202
         Check the password against existing credentials.
218
         Check the password against existing credentials.