Skip to content

Commit 7023e82

Browse files
committed
Gateway fallback to controller authentication
1 parent 71d129c commit 7023e82

File tree

2 files changed

+1053
-2
lines changed
  • ansible_base/authentication/authenticator_plugins
  • test_app/tests/authentication/authenticator_plugins

2 files changed

+1053
-2
lines changed

ansible_base/authentication/authenticator_plugins/local.py

Lines changed: 149 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
11
import logging
2+
from urllib.parse import urljoin
23

4+
import requests
5+
from django.contrib.auth import get_user_model
36
from django.contrib.auth.backends import ModelBackend
47
from django.core.exceptions import ValidationError
58
from django.utils.translation import gettext_lazy as _
9+
from requests.auth import HTTPBasicAuth
610

711
from ansible_base.authentication.authenticator_plugins.base import AbstractAuthenticatorPlugin, BaseAuthenticatorConfiguration
812
from ansible_base.authentication.utils.authentication import determine_username_from_uid, get_or_create_authenticator_user
913
from ansible_base.authentication.utils.claims import update_user_claims
14+
from ansible_base.lib.utils.settings import get_setting
1015

1116
logger = logging.getLogger('ansible_base.authentication.authenticator_plugins.local')
1217

18+
1319
# TODO: Change the validator to not allow it to be deleted or a second one added
1420

21+
UserModel = get_user_model()
22+
1523

1624
class LocalConfiguration(BaseAuthenticatorConfiguration):
1725
documentation_url = "https://docs.djangoproject.com/en/4.2/ref/contrib/auth/#django.contrib.auth.backends.ModelBackend"
@@ -44,7 +52,7 @@ def authenticate(self, request, username=None, password=None, **kwargs):
4452

4553
# Determine the user name for this authenticator, we have to call this so that we can "attach" to a pre-created user
4654
new_username = determine_username_from_uid(username, self.database_instance)
47-
# However we can't really accept a different username because we are the local authenticator imageine if:
55+
# However we can't really accept a different username because we are the local authenticator imagine if:
4856
# User "a" is from another authenticator and has an AuthenticatorUser
4957
# User "a" tried to login from local authenticator
5058
# The above function will return a username of "a<hash>"
@@ -53,6 +61,24 @@ def authenticate(self, request, username=None, password=None, **kwargs):
5361
return None
5462

5563
user = super().authenticate(request, username, password, **kwargs)
64+
controller_login_results = None
65+
if (
66+
not user
67+
and request
68+
and request.path.startswith('/api/gateway/v1/login/')
69+
and (controller_login_results := self._can_authenticate_from_controller(username, password))
70+
):
71+
logger.warning("User has been validated by controller, updating gateway user.")
72+
self.update_gateway_user(username, password)
73+
user = super().authenticate(request, username, password, **kwargs)
74+
elif not user:
75+
logger.info(
76+
"Fallback authentication condition not met: "
77+
f"username={username}, "
78+
f"request={'set' if request else 'None'}, "
79+
f"login_path={'True' if request and request.path.startswith('/api/gateway/v1/login/') else 'False'}, "
80+
f"controller_login_results={controller_login_results}"
81+
)
5682

5783
# This auth class doesn't create any new local users, but we still need to make sure
5884
# it has an AuthenticatorUser associated with it.
@@ -69,5 +95,126 @@ def authenticate(self, request, username=None, password=None, **kwargs):
6995
"is_superuser": user.is_superuser,
7096
},
7197
)
72-
7398
return update_user_claims(user, self.database_instance, [])
99+
100+
def _can_authenticate_from_controller(self, username, password):
101+
"""
102+
Check if a user exists in the AuthenticatorUser table with the local authenticator provider.
103+
If the user is valid, update the gateway users credentials with the controller credentials.
104+
"""
105+
try:
106+
UserModel._default_manager.get_by_natural_key(username)
107+
except UserModel.DoesNotExist:
108+
logger.warning(f"User '{username}' does not exist in the database.")
109+
return False
110+
111+
if controller_user := self._get_controller_user(username, password):
112+
# Validate controller_user has a ldap_dn field, if it is not None, then the user is a local user
113+
ldap_dn = controller_user.get("ldap_dn")
114+
if ldap_dn is None or ldap_dn != "":
115+
logger.warning(f"User '{username}' is an ldap user and can not be authenticated.")
116+
return False
117+
if controller_user.get('password', None) != "$encrypted$":
118+
logger.warning(f"User '{username}' is an enterprise user and can not be authenticated.")
119+
return False
120+
return True
121+
else:
122+
return False
123+
124+
def _get_controller_user(self, username: str, password: str):
125+
"""
126+
Get the user from the controller by making a request to the controller API /me/ endpoint.
127+
If the user is not found, return None.
128+
If the user is found, return the user.
129+
"""
130+
131+
controller_base_domain = get_setting('gateway_proxy_url')
132+
if not controller_base_domain:
133+
logger.warning("Controller authentication failed, unable to get controller base domain")
134+
return None
135+
controller_url = urljoin(controller_base_domain, "/api/controller/v2/me/")
136+
137+
timeout = get_setting('GRPC_SERVER_AUTH_SERVICE_TIMEOUT')
138+
timeout = self._convert_to_seconds(timeout)
139+
140+
try:
141+
response = requests.get(controller_url, auth=HTTPBasicAuth(username, password), timeout=int(timeout))
142+
response.raise_for_status()
143+
user_data = response.json()
144+
145+
# Check if count exists and equals 1
146+
count = user_data.get("count")
147+
if count != 1:
148+
logger.warning(f"Unable to authenticate user '{username}' with controller.")
149+
return None
150+
151+
# Check if results exists and is a non-empty list
152+
results = user_data.get("results")
153+
if not results or not isinstance(results, list) or len(results) == 0:
154+
logger.info(f"Unable to authenticate user '{username}' with controller. Invalid or empty results.")
155+
return None
156+
if not isinstance(results[0], dict):
157+
logger.warning(f"Unable to authenticate user '{username}' with controller. user was not a dictionary.")
158+
return False
159+
160+
return results[0]
161+
except requests.exceptions.HTTPError as http_err:
162+
logger.warning(f"HTTP error occurred: {http_err}")
163+
return None
164+
except requests.exceptions.ConnectionError as conn_err:
165+
logger.warning(f"Connection error occurred: {conn_err}")
166+
return None
167+
except requests.exceptions.Timeout as timeout_err:
168+
logger.warning(f"Timeout error occurred: {timeout_err}")
169+
return None
170+
except requests.exceptions.RequestException as err:
171+
logger.warning(f"An unexpected error occurred: {err}")
172+
return None
173+
except ValueError as json_err:
174+
logger.warning(f"JSON decode error occurred: {json_err}")
175+
return None
176+
except Exception as err:
177+
logger.warning(f"An unexpected error occurred: {err}")
178+
return None
179+
180+
def update_gateway_user(self, username, password):
181+
"""
182+
Update the gateway user with the controller credentials and set is_partially_migrated to False.
183+
"""
184+
user = UserModel._default_manager.get_by_natural_key(username)
185+
user.set_password(password)
186+
user.save(update_fields=['password'])
187+
logger.info(f"Updated user {username} gateway account")
188+
189+
def _convert_to_seconds(self, s):
190+
"""
191+
Converts a time string like '15s', '5m', '1h', '2d', '3w' to seconds.
192+
"""
193+
default = 10
194+
try:
195+
unit = s[-1].lower()
196+
value = int(s[:-1])
197+
198+
ret_val = 0
199+
# Check units
200+
if unit == '-':
201+
ret_val = default
202+
elif unit == 's':
203+
ret_val = value
204+
elif unit == 'm':
205+
ret_val = value * 60
206+
elif unit == 'h':
207+
ret_val = value * 3600 # 60 * 60
208+
elif unit == 'd':
209+
ret_val = value * 86400 # 60 * 60 * 24
210+
elif unit == 'w':
211+
ret_val = value * 604800 # 60 * 60 * 24 * 7
212+
else:
213+
ret_val = int(s)
214+
# If less than or equal to 0, return default
215+
if ret_val <= 0:
216+
ret_val = default
217+
return ret_val
218+
except Exception:
219+
logger.warning(f"Invalid duration format: '{s}'")
220+
return default

0 commit comments

Comments
 (0)