Skip to content

Commit df497e5

Browse files
authored
[AAP-48723] Update user.last_login_from when authenticating (#775)
1 parent f4ac23b commit df497e5

File tree

2 files changed

+168
-0
lines changed

2 files changed

+168
-0
lines changed

ansible_base/authentication/backend.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,8 @@ def authenticate(self, request, *args, **kwargs):
5656
return None
5757

5858
logger.info(f'User {user.username} logged in from authenticator with ID "{authenticator_id}"')
59+
if hasattr(user, "last_login_from"):
60+
user.last_login_from = authenticator_object.database_instance
61+
user.save(update_fields=['last_login_from'])
5962
return user
6063
return None

test_app/tests/authentication/test_backend.py

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,3 +172,168 @@ def authenticate(*args, **kwars):
172172
# Expect the log we emit
173173
with expected_log('ansible_base.authentication.backend.logger', "exception", "Exception raised while trying to authenticate with"):
174174
backend.AnsibleBaseAuth().authenticate(None)
175+
176+
177+
@pytest.mark.django_db
178+
def test_last_login_from_with_attribute(local_authenticator, random_user, expected_log):
179+
"""Test that last_login_from is set when user has the attribute."""
180+
# Add last_login_from attribute to user
181+
random_user.last_login_from = None
182+
183+
# Create a mock authenticator plugin object with proper structure
184+
mock_authenticator_plugin = mock.MagicMock()
185+
mock_authenticator_plugin.authenticate.return_value = random_user
186+
mock_authenticator_plugin.database_instance = local_authenticator
187+
188+
# Mock the save method to track calls
189+
with mock.patch.object(random_user, 'save') as mock_save:
190+
with mock.patch(
191+
"ansible_base.authentication.backend.get_authentication_backends",
192+
return_value={local_authenticator.id: mock_authenticator_plugin},
193+
):
194+
# Expected log message when user logs in
195+
with expected_log(
196+
'ansible_base.authentication.backend.logger',
197+
"info",
198+
f'User {random_user.username} logged in from authenticator with ID "{local_authenticator.id}"',
199+
):
200+
auth_return = backend.AnsibleBaseAuth().authenticate(None)
201+
202+
# Verify the user is returned
203+
assert auth_return == random_user
204+
205+
# Verify last_login_from was set to the authenticator database instance
206+
assert random_user.last_login_from == local_authenticator
207+
208+
# Verify save was called with the correct update_fields
209+
mock_save.assert_called_once_with(update_fields=['last_login_from'])
210+
211+
212+
@pytest.mark.django_db
213+
def test_last_login_from_without_attribute(local_authenticator, random_user, expected_log):
214+
"""Test that authentication works normally when user doesn't have last_login_from attribute."""
215+
# Ensure user doesn't have last_login_from attribute
216+
if hasattr(random_user, 'last_login_from'):
217+
delattr(random_user, 'last_login_from')
218+
219+
# Create a mock authenticator plugin object with proper structure
220+
mock_authenticator_plugin = mock.MagicMock()
221+
mock_authenticator_plugin.authenticate.return_value = random_user
222+
mock_authenticator_plugin.database_instance = local_authenticator
223+
224+
# Mock the save method to track calls
225+
with mock.patch.object(random_user, 'save') as mock_save:
226+
with mock.patch(
227+
"ansible_base.authentication.backend.get_authentication_backends",
228+
return_value={local_authenticator.id: mock_authenticator_plugin},
229+
):
230+
with expected_log(
231+
'ansible_base.authentication.backend.logger',
232+
"info",
233+
f'User {random_user.username} logged in from authenticator with ID "{local_authenticator.id}"',
234+
):
235+
auth_return = backend.AnsibleBaseAuth().authenticate(None)
236+
237+
# Verify the user is returned
238+
assert auth_return == random_user
239+
240+
# Verify save was never called since user doesn't have last_login_from
241+
mock_save.assert_not_called()
242+
243+
244+
@pytest.mark.django_db
245+
def test_last_login_from_multiple_authenticators(local_authenticator, github_enterprise_authenticator, random_user, expected_log):
246+
"""Test that last_login_from is set correctly when multiple authenticators are present."""
247+
# Add last_login_from attribute to user
248+
random_user.last_login_from = None
249+
250+
# Create mock authenticator plugin objects with proper structure
251+
mock_github_plugin = mock.MagicMock()
252+
mock_github_plugin.authenticate.return_value = None
253+
mock_github_plugin.database_instance = github_enterprise_authenticator
254+
255+
mock_local_plugin = mock.MagicMock()
256+
mock_local_plugin.authenticate.return_value = random_user
257+
mock_local_plugin.database_instance = local_authenticator
258+
259+
# Mock the save method to track calls
260+
with mock.patch.object(random_user, 'save') as mock_save:
261+
with mock.patch(
262+
"ansible_base.authentication.backend.get_authentication_backends",
263+
return_value={github_enterprise_authenticator.id: mock_github_plugin, local_authenticator.id: mock_local_plugin},
264+
):
265+
# Expected log message when user logs in
266+
with expected_log(
267+
'ansible_base.authentication.backend.logger',
268+
"info",
269+
f'User {random_user.username} logged in from authenticator with ID "{local_authenticator.id}"',
270+
):
271+
auth_return = backend.AnsibleBaseAuth().authenticate(None)
272+
273+
# Verify the user is returned
274+
assert auth_return == random_user
275+
276+
# Verify last_login_from was set to the local authenticator (the one that succeeded)
277+
assert random_user.last_login_from == local_authenticator
278+
279+
# Verify save was called with the correct update_fields
280+
mock_save.assert_called_once_with(update_fields=['last_login_from'])
281+
282+
283+
@pytest.mark.django_db
284+
def test_last_login_from_inactive_user(local_authenticator, random_user):
285+
"""Test that last_login_from is not set when user is inactive."""
286+
# Add last_login_from attribute to user but make user inactive
287+
random_user.last_login_from = None
288+
random_user.is_active = False
289+
290+
# Create a mock authenticator plugin object with proper structure
291+
mock_authenticator_plugin = mock.MagicMock()
292+
mock_authenticator_plugin.authenticate.return_value = random_user
293+
mock_authenticator_plugin.database_instance = local_authenticator
294+
295+
# Mock the save method to track calls
296+
with mock.patch.object(random_user, 'save') as mock_save:
297+
with mock.patch(
298+
"ansible_base.authentication.backend.get_authentication_backends",
299+
return_value={local_authenticator.id: mock_authenticator_plugin},
300+
):
301+
auth_return = backend.AnsibleBaseAuth().authenticate(None)
302+
303+
# Verify authentication failed (returns None for inactive user)
304+
assert auth_return is None
305+
306+
# Verify save was never called since authentication failed
307+
mock_save.assert_not_called()
308+
309+
# Verify last_login_from was not changed
310+
assert random_user.last_login_from is None
311+
312+
313+
@pytest.mark.django_db
314+
def test_last_login_from_social_auth_failed(local_authenticator, random_user):
315+
"""Test that last_login_from is not set when social auth pipeline fails."""
316+
# Add last_login_from attribute to user
317+
random_user.last_login_from = None
318+
319+
# Create a mock authenticator plugin object with proper structure
320+
mock_authenticator_plugin = mock.MagicMock()
321+
mock_authenticator_plugin.authenticate.return_value = SOCIAL_AUTH_PIPELINE_FAILED_STATUS
322+
mock_authenticator_plugin.database_instance = local_authenticator
323+
324+
# Mock the save method to track calls
325+
with mock.patch.object(random_user, 'save') as mock_save:
326+
with mock.patch(
327+
"ansible_base.authentication.backend.get_authentication_backends",
328+
return_value={local_authenticator.id: mock_authenticator_plugin},
329+
):
330+
auth_return = backend.AnsibleBaseAuth().authenticate(None)
331+
332+
# Verify authentication failed (returns None)
333+
assert auth_return is None
334+
335+
# Verify save was never called since authentication failed
336+
mock_save.assert_not_called()
337+
338+
# Verify last_login_from was not changed
339+
assert random_user.last_login_from is None

0 commit comments

Comments
 (0)