Skip to content

Commit ab12584

Browse files
Merge pull request #1642 from fledge-iot/FOGL-9494
FOGL-9494 Additional exception handling related to passwords has been incorporated into the authentication user model API, accompanied by unit tests
2 parents 8774840 + a2547ba commit ab12584

File tree

5 files changed

+112
-6
lines changed

5 files changed

+112
-6
lines changed

python/fledge/services/core/api/auth.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,6 @@ async def login(request):
110110
"""
111111
auth_method = request.auth_method if 'auth_method' in dir(request) else "any"
112112
data = await request.text()
113-
114113
try:
115114
# Check ott inside request payload.
116115
_data = json.loads(data)
@@ -175,14 +174,22 @@ async def login(request):
175174
host, port = peername
176175
try:
177176
uid, token, is_admin = await User.Objects.login(username, password, host)
178-
except (User.DoesNotExist, User.PasswordDoesNotMatch, ValueError) as ex:
179-
raise web.HTTPNotFound(reason=str(ex))
177+
except User.PasswordNotSetError as err:
178+
msg = str(err)
179+
raise web.HTTPBadRequest(reason=msg, body=json.dumps({"message": msg}))
180+
except (User.DoesNotExist, User.PasswordDoesNotMatch, ValueError) as err:
181+
msg = str(err)
182+
raise web.HTTPNotFound(reason=msg, body=json.dumps({"message": msg}))
180183
except User.PasswordExpired as ex:
181184
# delete all user token for this user
182185
await User.Objects.delete_user_tokens(str(ex))
183186
msg = 'Your password has been expired. Please set your password again.'
184187
_logger.warning(msg)
185188
raise web.HTTPUnauthorized(reason=msg)
189+
except Exception as exc:
190+
msg = str(exc)
191+
_logger.error(exc, "Failed to login.")
192+
raise web.HTTPInternalServerError(reason=msg, body=json.dumps({"message": msg}))
186193

187194
_logger.info("User with username:<{}> logged in successfully.".format(username))
188195
return web.json_response({"message": "Logged in successfully.", "uid": uid, "token": token, "admin": is_admin})

python/fledge/services/core/user_model.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ class DoesNotExist(Exception):
5959
class UserAlreadyExists(Exception):
6060
pass
6161

62+
class PasswordNotSetError(Exception):
63+
pass
64+
6265
class PasswordDoesNotMatch(Exception):
6366
pass
6467

@@ -382,7 +385,8 @@ async def login(cls, username, password, host):
382385
raise User.DoesNotExist('User does not exist')
383386

384387
found_user = result['rows'][0]
385-
388+
if not found_user.get('pwd'):
389+
raise User.PasswordNotSetError("Password is not set for this user.")
386390
# check age of password
387391
t1 = datetime.now()
388392
t2 = datetime.strptime(found_user['pwd_last_changed'], "%Y-%m-%d %H:%M:%S.%f")
@@ -475,7 +479,6 @@ async def login(cls, username, password, host):
475479
# Clear failed_attempts on successful login
476480
if int(found_user['failed_attempts']) > 0:
477481
await cls.update(found_user['id'],{'failed_attempts': 0})
478-
479482
uid, jwt_token, is_admin = await cls._get_new_token(storage_client, found_user, host)
480483
return uid, jwt_token, is_admin
481484

tests/unit/python/fledge/services/core/api/test_auth_mandatory.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1213,6 +1213,60 @@ async def test_reset_role_and_password(self, client, mocker):
12131213
patch_validate_token.assert_called_once_with(ADMIN_USER_HEADER['Authorization'])
12141214
patch_logger_debug.assert_called_once_with('Received %s request for %s', 'PUT', '/fledge/admin/2/reset')
12151215

1216+
@pytest.mark.parametrize("request_data, ret_val", [
1217+
({"username": "admin", "password": "fledge"}, (1, "token1", True)),
1218+
({"username": "user", "password": "fledge"}, (2, "token2", False))
1219+
])
1220+
async def test_login_auth_password(self, client, request_data, ret_val):
1221+
async def async_mock():
1222+
return ret_val
1223+
1224+
# Changed in version 3.8: patch() now returns an AsyncMock if the target is an async function.
1225+
if sys.version_info.major == 3 and sys.version_info.minor >= 8:
1226+
_rv = await async_mock()
1227+
else:
1228+
_rv = asyncio.ensure_future(async_mock())
1229+
1230+
with patch.object(middleware._logger, 'debug') as patch_logger:
1231+
with patch.object(User.Objects, 'login', return_value=_rv) as patch_user_login:
1232+
with patch.object(auth._logger, 'info') as patch_auth_logger:
1233+
resp = await client.post('/fledge/login', data=json.dumps(request_data))
1234+
assert 200 == resp.status
1235+
r = await resp.text()
1236+
actual = json.loads(r)
1237+
assert ret_val[0] == actual['uid']
1238+
assert ret_val[1] == actual['token']
1239+
assert ret_val[2] == actual['admin']
1240+
patch_auth_logger.assert_called_once_with('User with username:<{}> logged in successfully.'.format(
1241+
request_data['username']))
1242+
# TODO: host arg patch transport.request.extra_info
1243+
args, kwargs = patch_user_login.call_args
1244+
assert request_data['username'] == args[0]
1245+
assert request_data['password'] == args[1]
1246+
# patch_user_login.assert_called_once_with()
1247+
patch_logger.assert_called_once_with('Received %s request for %s', 'POST', '/fledge/login')
1248+
1249+
@pytest.mark.parametrize("exception_name, status_code, msg", [
1250+
(User.PasswordNotSetError, 400, 'Password is not set for this user.'),
1251+
(User.DoesNotExist, 404, 'User does not exist'),
1252+
(User.PasswordDoesNotMatch, 404, 'Username or Password do not match'),
1253+
(Exception, 500, 'Internal Server Error')
1254+
])
1255+
async def test_login_fails_when_password_auth_used_but_password_not_set(self, client, exception_name,
1256+
status_code, msg):
1257+
request_data_payload = {"username": "ranveer", "password": "Singh@123"}
1258+
with patch.object(middleware._logger, 'debug') as patch_logger:
1259+
with patch.object(User.Objects, 'login', side_effect=exception_name(msg)):
1260+
with patch.object(auth._logger, 'error') as patch_auth_logger:
1261+
resp = await client.post('/fledge/login', data=json.dumps(request_data_payload))
1262+
assert status_code == resp.status
1263+
assert msg == resp.reason
1264+
r = await resp.text()
1265+
actual = json.loads(r)
1266+
assert {'message': msg} == actual
1267+
patch_auth_logger.assert_not_called() if status_code != 500 else patch_auth_logger.assert_called()
1268+
patch_logger.assert_called_once_with('Received %s request for %s', 'POST', '/fledge/login')
1269+
12161270
@pytest.mark.parametrize("auth_method, request_data, ret_val", [
12171271
("certificate", "-----BEGIN CERTIFICATE----- Test -----END CERTIFICATE-----", (2, "token2", False))
12181272
])

tests/unit/python/fledge/services/core/api/test_auth_optional.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,10 @@ async def test_bad_login(self, client, request_data):
164164
({"username": "admin", "password": 123}, 404, User.PasswordDoesNotMatch, 'Username or Password do not match'),
165165
({"username": 1, "password": 1}, 404, ValueError, 'Username should be a valid string'),
166166
({"username": "user", "password": "fledge"}, 401, User.PasswordExpired,
167-
'Your password has been expired. Please set your password again.')
167+
'Your password has been expired. Please set your password again.'),
168+
({"username": "user1", "password": "blah"}, 400, User.PasswordNotSetError,
169+
'Password is not set for this user.')
170+
168171
])
169172
async def test_login_exception(self, client, request_data, status_code, exception_name, msg):
170173

tests/unit/python/fledge/services/core/test_user_model.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,45 @@ async def mock_get_category_item():
604604
assert payload == p
605605
mock_get_cat_patch.assert_called_once_with('password', 'expiration')
606606

607+
async def test_login_with_empty_password(self):
608+
async def mock_get_category_item():
609+
return {"value": "0"}
610+
611+
pwd_result = {'count': 1, 'rows': [{'pwd': '', 'id': 3, 'role_id': 2, 'access_method': 'cert',
612+
'pwd_last_changed': '', 'real_name': 'AJ', 'description': '',
613+
'hash_algorithm': 'SHA512', 'block_until': '', 'failed_attempts': 0}]}
614+
payload = {"return": ["pwd", "id", "role_id", "access_method",
615+
{"column": "pwd_last_changed", "format": "YYYY-MM-DD HH24:MI:SS.MS", "alias":
616+
"pwd_last_changed"}, "real_name", "description", "hash_algorithm", "block_until",
617+
"failed_attempts"],
618+
"where": {"column": "uname", "condition": "=", "value": "user",
619+
"and": {"column": "enabled", "condition": "=", "value": "t"}}}
620+
storage_client_mock = MagicMock(StorageClientAsync)
621+
622+
# Changed in version 3.8: patch() now returns an AsyncMock if the target is an async function.
623+
if sys.version_info.major == 3 and sys.version_info.minor >= 8:
624+
_rv1 = await mock_get_category_item()
625+
_rv2 = await mock_coro(pwd_result)
626+
else:
627+
_rv1 = asyncio.ensure_future(mock_get_category_item())
628+
_rv2 = asyncio.ensure_future(mock_coro(pwd_result))
629+
630+
with patch.object(connect, 'get_storage_async', return_value=storage_client_mock):
631+
with patch.object(ConfigurationManager, "get_category_item",
632+
return_value=_rv1) as mock_get_cat_patch:
633+
with patch.object(storage_client_mock, 'query_tbl_with_payload',
634+
return_value=_rv2) as query_tbl_patch:
635+
with pytest.raises(Exception) as excinfo:
636+
await User.Objects.login('user', 'blah', '0.0.0.0')
637+
assert str(excinfo.value) == 'Password is not set for this user.'
638+
assert excinfo.type is User.PasswordNotSetError
639+
assert issubclass(excinfo.type, Exception)
640+
args, kwargs = query_tbl_patch.call_args
641+
assert 'users' == args[0]
642+
p = json.loads(args[1])
643+
assert payload == p
644+
mock_get_cat_patch.assert_called_once_with('password', 'expiration')
645+
607646
async def test_login_age_pwd_expiration(self):
608647
async def mock_get_category_item():
609648
return {"value": "30"}

0 commit comments

Comments
 (0)