1
1
import logging
2
+ from urllib .parse import urljoin
2
3
4
+ import requests
5
+ from django .contrib .auth import get_user_model
3
6
from django .contrib .auth .backends import ModelBackend
4
7
from django .core .exceptions import ValidationError
5
8
from django .utils .translation import gettext_lazy as _
9
+ from requests .auth import HTTPBasicAuth
6
10
7
11
from ansible_base .authentication .authenticator_plugins .base import AbstractAuthenticatorPlugin , BaseAuthenticatorConfiguration
8
12
from ansible_base .authentication .utils .authentication import determine_username_from_uid , get_or_create_authenticator_user
9
13
from ansible_base .authentication .utils .claims import update_user_claims
14
+ from ansible_base .lib .utils .settings import get_setting
10
15
11
16
logger = logging .getLogger ('ansible_base.authentication.authenticator_plugins.local' )
12
17
18
+
13
19
# TODO: Change the validator to not allow it to be deleted or a second one added
14
20
21
+ UserModel = get_user_model ()
22
+
15
23
16
24
class LocalConfiguration (BaseAuthenticatorConfiguration ):
17
25
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):
44
52
45
53
# Determine the user name for this authenticator, we have to call this so that we can "attach" to a pre-created user
46
54
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:
48
56
# User "a" is from another authenticator and has an AuthenticatorUser
49
57
# User "a" tried to login from local authenticator
50
58
# The above function will return a username of "a<hash>"
@@ -53,6 +61,24 @@ def authenticate(self, request, username=None, password=None, **kwargs):
53
61
return None
54
62
55
63
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
+ )
56
82
57
83
# This auth class doesn't create any new local users, but we still need to make sure
58
84
# it has an AuthenticatorUser associated with it.
@@ -69,5 +95,126 @@ def authenticate(self, request, username=None, password=None, **kwargs):
69
95
"is_superuser" : user .is_superuser ,
70
96
},
71
97
)
72
-
73
98
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