Skip to content

Commit b344d21

Browse files
AAP-49757: Add setting ANSIBLE_BASE_AUTHENTICATION_LOCAL_FALLBACK_AUTHENTICATORS (ansible#878)
- Added comprehensive tests for the new `ANSIBLE_BASE_AUTHENTICATION_LOCAL_FALLBACK_AUTHENTICATORS` setting - Added detailed documentation explaining how to configure and implement fallback authenticators
1 parent fb34d6e commit b344d21

File tree

3 files changed

+127
-1
lines changed

3 files changed

+127
-1
lines changed

ansible_base/authentication/management/commands/authenticators.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
except ImportError:
66
HAS_TABULATE = False
77

8+
from django.conf import settings
89
from django.contrib.auth import get_user_model
910
from django.core.management.base import BaseCommand, CommandError
1011
from django.utils.translation import gettext_lazy as _
@@ -82,11 +83,14 @@ def initialize_authenticators(self):
8283
creator = None
8384
self.stderr.write("Neither system user nor admin user were defined, local authenticator will be created without created_by set")
8485

86+
fallback_authenticators = getattr(settings, "ANSIBLE_BASE_AUTHENTICATION_LOCAL_FALLBACK_AUTHENTICATORS", [])
87+
configuration = {"fallback_authentication": fallback_authenticators} if fallback_authenticators else {}
88+
8589
Authenticator.objects.create(
8690
name='Local Database Authenticator',
8791
enabled=True,
8892
create_objects=True,
89-
configuration={},
93+
configuration=configuration,
9094
created_by=creator,
9195
modified_by=creator,
9296
remove_users=False,

docs/apps/authentication/authentication.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,72 @@ ANSIBLE_BASE_AUTHENTICATOR_CLASS_PREFIXES = ["ansible_base.authentication.authen
5959

6060
If you are going to create a different class to hold the plugins you can change or add to this as needed.
6161

62+
#### ANSIBLE_BASE_AUTHENTICATION_LOCAL_FALLBACK_AUTHENTICATORS
63+
This setting allows you to configure fallback authenticators for the local database authenticator plugin. When the `authenticators --initialize` management command creates the default local authenticator, it will include these fallback authenticators in the configuration.
64+
65+
```
66+
ANSIBLE_BASE_AUTHENTICATION_LOCAL_FALLBACK_AUTHENTICATORS = [
67+
"my_service.authentication.fallbacks.my_fallback_service",
68+
"another_app.auth.fallback_handler"
69+
]
70+
```
71+
72+
The setting expects a list of Python module paths. Each path should point to a module containing a `FallbackAuthenticator` class. When set, these authenticators will be configured as fallback options in the local authenticator's configuration under the `fallback_authentication` key. If the setting is not provided or is empty, no fallback configuration will be added.
73+
74+
This is useful when you want the local authenticator to fall back to other authentication methods when local authentication fails. The fallback authenticators are tried in the order specified.
75+
76+
##### Creating a FallbackAuthenticator Class
77+
78+
Each fallback authenticator module must contain a class named `FallbackAuthenticator` with an `authenticate` method that takes the same arguments as an authenticator plugin:
79+
80+
```python
81+
class FallbackAuthenticator:
82+
def authenticate(self, request, username=None, password=None, **kwargs):
83+
"""
84+
Authenticate a user using fallback authentication logic.
85+
86+
Args:
87+
request: The HTTP request object
88+
username: The username to authenticate
89+
password: The password to authenticate
90+
**kwargs: Additional authentication parameters
91+
92+
Returns:
93+
User object if authentication successful, None otherwise
94+
"""
95+
# Your custom authentication logic here
96+
# Return a User object on success, None on failure
97+
pass
98+
```
99+
100+
Example implementation:
101+
102+
```python
103+
# my_service/authentication/fallbacks/my_fallback_service.py
104+
import logging
105+
from django.contrib.auth import get_user_model
106+
107+
logger = logging.getLogger(__name__)
108+
User = get_user_model()
109+
110+
class FallbackAuthenticator:
111+
def authenticate(self, request, username=None, password=None, **kwargs):
112+
# Example: authenticate against an external service
113+
if self._validate_with_external_service(username, password):
114+
try:
115+
user = User.objects.get(username=username)
116+
logger.info(f"Fallback authentication successful for {username}")
117+
return user
118+
except User.DoesNotExist:
119+
logger.warning(f"User {username} authenticated but not found in database")
120+
return None
121+
return None
122+
123+
def _validate_with_external_service(self, username, password):
124+
# Your external authentication logic
125+
return False
126+
```
127+
62128
#### REST_FRAMEWORK
63129
If you are using DRF and enable django-ansible-base authentication we prepend our authentication class to your REST_FRAMEWORK settings if our class is not already present:
64130
```

test_app/tests/authentication/management/test_authenticators.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,3 +200,59 @@ def test_authenticators_cli_enable_disable_nonexisting(flag):
200200
call_command('authenticators', flag, 1337, stdout=out, stderr=err)
201201

202202
assert "Authenticator 1337 does not exist" in str(e.value)
203+
204+
205+
@pytest.mark.parametrize(
206+
"fallback_setting,expected_config",
207+
[
208+
(["test_fallback_auth"], {"fallback_authentication": ["test_fallback_auth"]}),
209+
(["auth1", "auth2"], {"fallback_authentication": ["auth1", "auth2"]}),
210+
([], {}),
211+
(None, {}),
212+
],
213+
)
214+
def test_authenticators_cli_initialize_with_fallback_setting(django_user_model, fallback_setting, expected_config):
215+
"""
216+
Test that the ANSIBLE_BASE_AUTHENTICATION_LOCAL_FALLBACK_AUTHENTICATORS setting
217+
is properly applied when initializing the local authenticator.
218+
"""
219+
out = StringIO()
220+
err = StringIO()
221+
222+
# Ensure no authenticators exist initially
223+
assert Authenticator.objects.count() == 0
224+
225+
# Create admin user for the test
226+
django_user_model.objects.create(username="admin")
227+
228+
with override_settings(ANSIBLE_BASE_AUTHENTICATION_LOCAL_FALLBACK_AUTHENTICATORS=fallback_setting):
229+
call_command('authenticators', "--initialize", stdout=out, stderr=err)
230+
231+
assert Authenticator.objects.count() == 1
232+
authenticator = Authenticator.objects.first()
233+
assert authenticator.name == "Local Database Authenticator"
234+
assert authenticator.type == "ansible_base.authentication.authenticator_plugins.local"
235+
assert authenticator.configuration == expected_config
236+
237+
238+
def test_authenticators_cli_initialize_fallback_setting_preserves_existing_config(django_user_model, local_authenticator):
239+
"""
240+
Test that when a local authenticator already exists, the initialize command
241+
doesn't modify its configuration even if the fallback setting is present.
242+
"""
243+
out = StringIO()
244+
err = StringIO()
245+
246+
# Store the original configuration
247+
original_config = local_authenticator.configuration.copy()
248+
249+
with override_settings(ANSIBLE_BASE_AUTHENTICATION_LOCAL_FALLBACK_AUTHENTICATORS=["test_fallback"]):
250+
call_command('authenticators', "--initialize", stdout=out, stderr=err)
251+
252+
# Should still only have one authenticator
253+
assert Authenticator.objects.count() == 1
254+
authenticator = Authenticator.objects.first()
255+
256+
# Configuration should remain unchanged
257+
assert authenticator.configuration == original_config
258+
assert "Local authenticator already exists, skipping" in out.getvalue()

0 commit comments

Comments
 (0)