11import logging
2+ from urllib .parse import urljoin
23
4+ import requests
5+ from django .contrib .auth import get_user_model
36from django .contrib .auth .backends import ModelBackend
47from django .core .exceptions import ValidationError
58from django .utils .translation import gettext_lazy as _
9+ from requests .auth import HTTPBasicAuth
610
711from ansible_base .authentication .authenticator_plugins .base import AbstractAuthenticatorPlugin , BaseAuthenticatorConfiguration
812from ansible_base .authentication .utils .authentication import determine_username_from_uid , get_or_create_authenticator_user
913from ansible_base .authentication .utils .claims import update_user_claims
14+ from ansible_base .lib .utils .settings import get_setting
1015
1116logger = 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
1624class 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