Skip to content

Commit b22c431

Browse files
authored
Merge pull request #4641 from dr-antimonious/master
[feat] Store hashed pwds in server config
2 parents 6c7fcb0 + cfc0b08 commit b22c431

4 files changed

Lines changed: 103 additions & 18 deletions

File tree

docs/web/authentication.md

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,27 @@ option of `CodeChecker server` command.
102102

103103
## <i>Dictionary</i> authentication <a name="dictionary-authentication"></a>
104104

105-
> *Note*: Storing passwords in a plain text file is *strongly discouraged* due to security risks. If no other option is available, ensure that the file permissions are restricted to 0600 to limit access *only* to the file owner.
106-
107-
The `authentication.method_dictionary` contains a plaintext `username:password`
108-
credentials for authentication. If the user's login matches any of the
109-
credentials listed, the user will be authenticated.
105+
> *Note*: Storing passwords in a plain text file is *strongly discouraged* due to security risks.
106+
If no other option is available, ensure that the file permissions are restricted to 0600
107+
to limit access *only* to the file owner.
108+
109+
The `authentication.method_dictionary` may contain several formats of user credentials:
110+
111+
- `username:password`
112+
- legacy user credential format, **not recommended**
113+
- `username:password_hash:hash_algorithm`
114+
- recommended user credential format
115+
- `username:password_hash:hash_algorithm:salt`
116+
- highest security user credential format, `password_hash`
117+
is calculated by appending `salt` to the provided password
118+
119+
Supported `hash_algorithm` string values depend on hash algorithms supported by
120+
the [hashlib](https://docs.python.org/3/library/hashlib.html#hash-algorithms)
121+
Python module. Examples of supported `hash_algorithm` values:
122+
- `sha224`, `sha256`, `sha384`, `sha512`
123+
- added in Python 3.6: `sha3_224`, `sha3_256`, `sha3_384`, `sha3_512`
124+
125+
If the user's login matches any of the credentials listed, the user will be authenticated.
110126

111127
Groups are configured in a map which maps to each username the list of groups
112128
the user belongs to.

web/server/codechecker_server/session_manager.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import uuid
1616

1717
from datetime import datetime
18+
import hashlib
1819
from typing import Optional
1920

2021
from codechecker_common.compatibility.multiprocessing import cpu_count
@@ -606,12 +607,41 @@ def __try_auth_dictionary(self, auth_string):
606607
if not method_config:
607608
return False
608609

609-
valid = self.__is_method_enabled('dictionary') and \
610-
auth_string in method_config.get('auths')
611-
if not valid:
610+
if not self.__is_method_enabled('dictionary'):
612611
return False
613612

614-
username = SessionManager.get_user_name(auth_string)
613+
auth_string_split = auth_string.split(':', 1)
614+
username = auth_string_split[0]
615+
616+
saved_auth_string = [auth for auth in method_config.get('auths')
617+
if auth.split(':', 1)[0] == username]
618+
if len(saved_auth_string) == 0:
619+
return False
620+
621+
saved_auth_string = saved_auth_string[0]
622+
if saved_auth_string != auth_string:
623+
saved_auth_string_split = saved_auth_string.split(':', 3)
624+
try:
625+
hash_algorithm = saved_auth_string_split[2]
626+
except IndexError:
627+
return False
628+
629+
if not hasattr(hashlib, hash_algorithm):
630+
return False
631+
632+
password = auth_string_split[1]
633+
try:
634+
salt = saved_auth_string_split[3]
635+
password += salt
636+
except IndexError:
637+
pass
638+
639+
password = password.encode("utf-8")
640+
saved_password_hash = saved_auth_string_split[1]
641+
if not saved_password_hash == \
642+
getattr(hashlib, hash_algorithm)(password).hexdigest():
643+
return False
644+
615645
group_list = method_config['groups'][username] if \
616646
'groups' in method_config and \
617647
username in method_config['groups'] else []

web/tests/functional/authentication/test_authentication.py

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,26 @@ def test_privileged_access(self):
7676
auth_client.performLogin("Username:Password", None)
7777
print("Empty credentials gave us a token!")
7878

79+
with self.assertRaises(RequestFailed):
80+
auth_client.performLogin("Username:Password", "colon:my:pass:word")
81+
print("Incorrect password gave us a token!")
82+
83+
with self.assertRaises(RequestFailed):
84+
auth_client.performLogin("Username:Password", "colon:mypassword")
85+
print("Incorrect password gave us a token!")
86+
87+
with self.assertRaises(RequestFailed):
88+
auth_client.performLogin("Username:Password",
89+
"hashtest1:hashtest1")
90+
print(("Pre-saved credentials with invalid "
91+
"hash algorithm gave us a token!"))
92+
93+
with self.assertRaises(RequestFailed):
94+
auth_client.performLogin("Username:Password",
95+
"hashtest2:hashtest2")
96+
print("Pre-saved credentials with invalid "
97+
"hash value gave us a token!")
98+
7999
# A non-authenticated session should return an empty user.
80100
user = auth_client.getLoggedInUser()
81101
self.assertEqual(user, "")
@@ -136,14 +156,22 @@ def test_privileged_access(self):
136156

137157
self.assertTrue(result, "Server did not allow us to destroy session.")
138158

139-
self.session_token = auth_client.performLogin(
140-
"Username:Password", "colon:my:password")
141-
self.assertIsNotNone(self.session_token,
142-
"Valid credentials didn't give us a token!")
159+
valid_credentials = ["colon:my:password",
160+
"colon123:my:password",
161+
"hashtest3:hashtest3",
162+
"hashtest4:hashtest4",
163+
"hashtest5:hashtest5"]
143164

144-
result = auth_client.destroySession()
165+
for credential in valid_credentials:
166+
self.session_token = auth_client.performLogin(
167+
"Username:Password", credential)
168+
self.assertIsNotNone(self.session_token,
169+
"Valid credentials didn't give us a token!")
145170

146-
self.assertTrue(result, "Server did not allow us to destroy session.")
171+
result = auth_client.destroySession()
172+
173+
self.assertTrue(result,
174+
"Server did not allow us to destroy session.")
147175

148176
# Kill the session token that was created by login() too.
149177
codechecker.logout(self._test_cfg['codechecker_cfg'],

web/tests/libtest/env.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -362,9 +362,20 @@ def enable_auth(workspace):
362362
scfg_dict["authentication"]["super_user"] = "root"
363363
scfg_dict["authentication"]["method_dictionary"]["enabled"] = True
364364
scfg_dict["authentication"]["method_dictionary"]["auths"] = \
365-
["cc:test", "john:doe", "admin:admin123", "colon:my:password",
366-
"admin_group_user:admin123", "regex_admin:blah",
367-
"permission_view_user:pvu", "root:root"]
365+
["cc:test", "john:doe", "admin:admin123", "colon123:my:password",
366+
"colon:my:password", "admin_group_user:admin123",
367+
"regex_admin:blah", "permission_view_user:pvu", "root:root",
368+
"hashtest1:hashtest1:this_will_fail",
369+
"hashtest2:this_will_fail_too:sha512",
370+
("hashtest3:9d49be0aa9430dc908e6f6ecd1eff1c253e3aefd6df7ea"
371+
"daeb2a66b797d9bba842f16963d4cc7a8dbb1b61c0f75cabb52f48a9"
372+
"0d6b57b453ae4f85c4352e269f:sha512"),
373+
("hashtest4:8b440a15aba9665761a279b7cd12659bf1b6527bdbe6e4"
374+
"3c2ef97026a05d1efe9321b6aa6fec32c2f00aaebc2baa6aab5dc54b"
375+
"bd4c9f9adc0d7d3744f5b7f3df:sha3_512"),
376+
("hashtest5:33a3060019fb2bb16b4eb9eb9ec59bee4ccc658a9e3186"
377+
"68e6ff0b142d523a0de571adf979428872eb2eb3fd34821687e09b92"
378+
"f765ebc5ddbf9ea3cae76d292f:sha3_512:with:salt")]
368379
scfg_dict["authentication"]["method_dictionary"]["groups"] = \
369380
{"admin_group_user": ["admin_GROUP"]}
370381
scfg_dict["authentication"]["regex_groups"]["enabled"] = True

0 commit comments

Comments
 (0)