Removing "globalRequest" cache and passing through received request object to authenticators

This commit is contained in:
Adolfo Gómez García 2021-10-26 21:15:07 +02:00
parent e999e5acf8
commit 590f3191ac
11 changed files with 112 additions and 167 deletions

View File

@ -176,7 +176,7 @@ class Login(Handler):
password = 'xdaf44tgas4xd5ñasdłe4g€@#½|«ð2' # Extrange password if credential left empty. Value is not important, just not empty
logger.debug('Auth obj: %s', auth)
user = authenticate(username, password, auth, True)
user = authenticate(username, password, auth, self._request, True)
if user is None: # invalid credentials
# Sleep a while here to "prottect"
time.sleep(3) # Wait 3 seconds if credentials fails for "protection"

View File

@ -38,10 +38,12 @@ from django.utils.translation import ugettext_noop as _
from uds.core import auths
from uds.core.util import net
from uds.core.ui import gui
from uds.core.util.request import getRequest, ExtendedHttpRequest
logger = logging.getLogger(__name__)
if typing.TYPE_CHECKING:
from uds.core.util.request import ExtendedHttpRequest
class IPAuth(auths.Authenticator):
acceptProxy = gui.CheckBoxField(
@ -52,15 +54,17 @@ class IPAuth(auths.Authenticator):
'If checked, requests via proxy will get FORWARDED ip address'
' (take care with this bein checked, can take internal IP addresses from internet)'
),
tab=gui.ADVANCED_TAB
tab=gui.ADVANCED_TAB,
)
visibleFromNets = gui.TextField(
order=50,
label=_('Visible only from this networks'),
defvalue='',
tooltip=_('This authenticator will be visible only from these networks. Leave empty to allow all networks'),
tab=gui.ADVANCED_TAB
tooltip=_(
'This authenticator will be visible only from these networks. Leave empty to allow all networks'
),
tab=gui.ADVANCED_TAB,
)
typeName = _('IP Authenticator')
@ -75,8 +79,8 @@ class IPAuth(auths.Authenticator):
blockUserOnLoginFailures = False
def getIp(self) -> str:
ip = getRequest().ip_proxy if self.acceptProxy.isTrue() else getRequest().ip
def getIp(self, request: 'ExtendedHttpRequest') -> str:
ip = request.ip_proxy if self.acceptProxy.isTrue() else request.ip
logger.debug('Client IP: %s', ip)
return ip
@ -90,9 +94,15 @@ class IPAuth(auths.Authenticator):
except Exception as e:
logger.error('Invalid network for IP auth: %s', e)
def authenticate(self, username: str, credentials: str, groupsManager: 'auths.GroupsManager') -> bool:
def authenticate(
self,
username: str,
credentials: str,
groupsManager: 'auths.GroupsManager',
request: 'ExtendedHttpRequest',
) -> bool:
# If credentials is a dict, that can't be sent directly from web interface, we allow entering
if username == self.getIp():
if username == self.getIp(request):
self.getGroups(username, groupsManager)
return True
return False
@ -106,11 +116,19 @@ class IPAuth(auths.Authenticator):
return True
return False
def internalAuthenticate(self, username: str, credentials: str, groupsManager: 'auths.GroupsManager') -> bool:
def internalAuthenticate(
self,
username: str,
credentials: str,
groupsManager: 'auths.GroupsManager',
request: 'ExtendedHttpRequest',
) -> bool:
# In fact, username does not matter, will get IP from request
username = self.getIp() # Override provided username and use source IP
username = self.getIp(request) # Override provided username and use source IP
self.getGroups(username, groupsManager)
if groupsManager.hasValidGroups() and self.dbAuthenticator().isValidUser(username, True):
if groupsManager.hasValidGroups() and self.dbAuthenticator().isValidUser(
username, True
):
return True
return False
@ -124,7 +142,7 @@ class IPAuth(auths.Authenticator):
def getJavascript(self, request: 'ExtendedHttpRequest') -> typing.Optional[str]:
# We will authenticate ip here, from request.ip
# If valid, it will simply submit form with ip submited and a cached generated random password
ip = self.getIp()
ip = self.getIp(request)
gm = auths.GroupsManager(self.dbAuthenticator())
self.getGroups(ip, gm)
@ -134,7 +152,9 @@ class IPAuth(auths.Authenticator):
}}
setVal("id_user", "{ip}");
setVal("id_password", "{passwd}");
document.getElementById("loginform").submit();'''.format(ip=ip, passwd='')
document.getElementById("loginform").submit();'''.format(
ip=ip, passwd=''
)
return 'alert("invalid authhenticator"); window.location.reload();'

View File

@ -43,12 +43,12 @@ from uds.core import auths
from uds.core.ui import gui
from uds.core.managers import cryptoManager
from uds.core.util.state import State
from uds.core.util.request import getRequest
from uds.core.auths.auth import authLogLogin
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
from uds import models
from uds.core.util.request import ExtendedHttpRequest
logger = logging.getLogger(__name__)
@ -92,22 +92,13 @@ class InternalDBAuth(auths.Authenticator):
tab=gui.ADVANCED_TAB,
)
def getIp(self) -> str:
ip = (
getRequest().ip_proxy if self.acceptProxy.isTrue() else getRequest().ip
) # pylint: disable=maybe-no-member
if self.reverseDns.isTrue():
try:
return str(
dns.resolver.query(dns.reversename.from_address(ip), 'PTR')[0]
)
except Exception:
pass
return ip
def transformUsername(self, username: str) -> str:
def transformUsername(self, username: str, request: 'ExtendedHttpRequest') -> str:
if self.differentForEachHost.isTrue():
newUsername = self.getIp() + '-' + username
newUsername = (
(request.ip_proxy if self.acceptProxy.isTrue() else request.ip)
+ '-'
+ username
)
# Duplicate basic user into username.
auth = self.dbAuthenticator()
# "Derived" users will belong to no group at all, because we will extract groups from "base" user
@ -129,14 +120,18 @@ class InternalDBAuth(auths.Authenticator):
return username
def authenticate(
self, username: str, credentials: str, groupsManager: 'auths.GroupsManager'
self,
username: str,
credentials: str,
groupsManager: 'auths.GroupsManager',
request: 'ExtendedHttpRequest',
) -> bool:
logger.debug('Username: %s, Password: %s', username, credentials)
dbAuth = self.dbAuthenticator()
try:
user: 'models.User' = dbAuth.users.get(name=username, state=State.ACTIVE)
except Exception:
authLogLogin(getRequest(), self.dbAuthenticator(), username, 'Invalid user')
authLogLogin(request, self.dbAuthenticator(), username, 'Invalid user')
return False
if user.parent: # Direct auth not allowed for "derived" users
@ -146,7 +141,8 @@ class InternalDBAuth(auths.Authenticator):
if cryptoManager().checkHash(credentials, user.password):
groupsManager.validate([g.name for g in user.groups.all()])
return True
authLogLogin(getRequest(), self.dbAuthenticator(), username, 'Invalid password')
authLogLogin(request, self.dbAuthenticator(), username, 'Invalid password')
return False
def getGroups(self, username: str, groupsManager: 'auths.GroupsManager'):

View File

@ -39,12 +39,11 @@ from uds.core.ui import gui
from uds.core import auths
from uds.core.managers import cryptoManager
from uds.core.auths.auth import authLogLogin
from uds.core.util.request import getRequest
from . import client
if typing.TYPE_CHECKING:
pass
from uds.core.util.request import ExtendedHttpRequest
logger = logging.getLogger(__name__)
@ -116,8 +115,8 @@ class RadiusAuth(auths.Authenticator):
def initialize(self, values: typing.Optional[typing.Dict[str, typing.Any]]) -> None:
pass
def radiusClient(self) -> client.RadiusClient:
""" Return a new radius client . """
def radiusClient(self) -> client.RadiusClient:
"""Return a new radius client ."""
return client.RadiusClient(
self.server.value,
self.secret.value.encode(),
@ -126,13 +125,22 @@ class RadiusAuth(auths.Authenticator):
)
def authenticate(
self, username: str, credentials: str, groupsManager: 'auths.GroupsManager'
self,
username: str,
credentials: str,
groupsManager: 'auths.GroupsManager',
request: 'ExtendedHttpRequest',
) -> bool:
try:
connection = self.radiusClient()
groups = connection.authenticate(username=username, password=credentials)
except Exception:
authLogLogin(getRequest(), self.dbAuthenticator(), username, 'Access denied by Raiuds')
authLogLogin(
request,
self.dbAuthenticator(),
username,
'Access denied by Raiuds',
)
return False
if self.globalGroup.value.strip():
@ -161,23 +169,23 @@ class RadiusAuth(auths.Authenticator):
return super().removeUser(username)
@staticmethod
def test(env, data):
""" Test the connection to the server . """
def test(env, data):
"""Test the connection to the server ."""
try:
auth = RadiusAuth(None, env, data) # type: ignore
return auth.testConnection()
except Exception as e:
logger.error(
"Exception found testing Radius auth %s: %s", e.__class__, e
)
logger.error("Exception found testing Radius auth %s: %s", e.__class__, e)
return [False, _('Error testing connection')]
def testConnection(self):
""" Test connection to Radius Server """
def testConnection(self):
"""Test connection to Radius Server"""
try:
connection = self.radiusClient()
# Reply is not important...
connection.authenticate(cryptoManager().randomString(10), cryptoManager().randomString(10))
connection.authenticate(
cryptoManager().randomString(10), cryptoManager().randomString(10)
)
except client.RadiusAuthenticationError as e:
pass
except Exception:

View File

@ -43,7 +43,6 @@ from uds.core import auths
from uds.core.ui import gui
from uds.core.util import ldaputil
from uds.core.auths.auth import authLogLogin
from uds.core.util.request import getRequest
try:
# pylint: disable=no-name-in-module
@ -53,8 +52,8 @@ except Exception:
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
from django.http import HttpRequest # pylint: disable=ungrouped-imports
from uds.core.environment import Environment
from uds.core.util.request import ExtendedHttpRequest
from uds import models
logger = logging.getLogger(__name__)
@ -485,7 +484,11 @@ class RegexLdap(auths.Authenticator):
return ' '.join(self.__processField(self._userNameAttr, user))
def authenticate(
self, username: str, credentials: str, groupsManager: 'auths.GroupsManager'
self,
username: str,
credentials: str,
groupsManager: 'auths.GroupsManager',
request: 'ExtendedHttpRequest',
) -> bool:
"""
Must authenticate the user.
@ -502,7 +505,7 @@ class RegexLdap(auths.Authenticator):
if usr is None:
authLogLogin(
getRequest(), self.dbAuthenticator(), username, 'Invalid user'
request, self.dbAuthenticator(), username, 'Invalid user'
)
return False
@ -513,7 +516,7 @@ class RegexLdap(auths.Authenticator):
) # Will raise an exception if it can't connect
except:
authLogLogin(
getRequest(), self.dbAuthenticator(), username, 'Invalid password'
request, self.dbAuthenticator(), username, 'Invalid password'
)
return False

View File

@ -42,11 +42,10 @@ from uds.core.ui import gui
from uds.core import auths
from uds.core.util import ldaputil
from uds.core.auths.auth import authLogLogin
from uds.core.util.request import getRequest
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
from django.http import HttpRequest # pylint: disable=ungrouped-imports
from uds.core.util.request import ExtendedHttpRequest
logger = logging.getLogger(__name__)
@ -385,7 +384,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
).strip()
def authenticate(
self, username: str, credentials: str, groupsManager: 'auths.GroupsManager'
self, username: str, credentials: str, groupsManager: 'auths.GroupsManager', request: 'ExtendedHttpRequest'
) -> bool:
'''
Must authenticate the user.
@ -402,7 +401,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
if user is None:
authLogLogin(
getRequest(), self.dbAuthenticator(), username, 'Invalid user'
request, self.dbAuthenticator(), username, 'Invalid user'
)
return False
@ -413,7 +412,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
) # Will raise an exception if it can't connect
except:
authLogLogin(
getRequest(), self.dbAuthenticator(), username, 'Invalid password'
request, self.dbAuthenticator(), username, 'Invalid password'
)
return False

View File

@ -202,20 +202,19 @@ def denyNonAuthenticated(
def __registerUser(
authenticator: Authenticator, authInstance: AuthenticatorInstance, username: str
authenticator: Authenticator,
authInstance: AuthenticatorInstance,
username: str,
request: 'ExtendedHttpRequest',
) -> typing.Optional[User]:
"""
Check if this user already exists on database with this authenticator, if don't, create it with defaults
This will work correctly with both internal or externals cause we first authenticate the user, if internal and user do not exists in database
authenticate will return false, if external and return true, will create a reference in database
"""
from uds.core.util.request import getRequest
username = authInstance.transformUsername(username)
username = authInstance.transformUsername(username, request)
logger.debug('Transformed username: %s', username)
request = getRequest()
usr = authenticator.getOrCreateUser(username, username)
usr.real_name = authInstance.getRealName(username)
usr.save()
@ -242,6 +241,7 @@ def authenticate(
username: str,
password: str,
authenticator: Authenticator,
request: 'ExtendedHttpRequest',
useInternalAuthenticate: bool = False,
) -> typing.Optional[User]:
"""
@ -249,6 +249,7 @@ def authenticate(
@param username: username to authenticate
@param password: password to authenticate this user
@param authenticator: Authenticator (database object) used to authenticate with provided credentials
@param request: Request object
@param useInternalAuthenticate: If True, tries to authenticate user using "internalAuthenticate". If false, it uses "authenticate".
This is so because in some situations we may want to use a "trusted" method (internalAuthenticate is never invoked directly from web)
@return: None if authentication fails, User object (database object) if authentication is o.k.
@ -269,9 +270,9 @@ def authenticate(
gm = auths.GroupsManager(authenticator)
authInstance = authenticator.getInstance()
if useInternalAuthenticate is False:
res = authInstance.authenticate(username, password, gm)
res = authInstance.authenticate(username, password, gm, request)
else:
res = authInstance.internalAuthenticate(username, password, gm)
res = authInstance.internalAuthenticate(username, password, gm, request)
if res is False:
return None
@ -286,11 +287,13 @@ def authenticate(
)
return None
return __registerUser(authenticator, authInstance, username)
return __registerUser(authenticator, authInstance, username, request)
def authenticateViaCallback(
authenticator: Authenticator, params: typing.Any, request: 'ExtendedHttpRequestWithUser'
authenticator: Authenticator,
params: typing.Any,
request: 'ExtendedHttpRequestWithUser',
) -> typing.Optional[User]:
"""
Given an username, this method will get invoked whenever the url for a callback
@ -322,7 +325,7 @@ def authenticateViaCallback(
if username is None or username == '' or gm.hasValidGroups() is False:
raise auths.exceptions.InvalidUserException('User doesn\'t has access to UDS')
return __registerUser(authenticator, authInstance, username)
return __registerUser(authenticator, authInstance, username, request)
def authCallbackUrl(authenticator: Authenticator) -> str:

View File

@ -44,7 +44,7 @@ if typing.TYPE_CHECKING:
HttpResponse,
) # pylint: disable=ungrouped-imports
from uds.core.environment import Environment
from uds.core.util.request import ExtendedHttpRequestWithUser
from uds.core.util.request import ExtendedHttpRequestWithUser, ExtendedHttpRequest
from uds import models
from .groups_manager import GroupsManager
@ -292,7 +292,7 @@ class Authenticator(Module): # pylint: disable=too-many-public-methods
return []
def authenticate(
self, username: str, credentials: str, groupsManager: 'GroupsManager'
self, username: str, credentials: str, groupsManager: 'GroupsManager', request: 'ExtendedHttpRequest'
) -> bool:
"""
This method must be overriden, and is responsible for authenticating
@ -340,7 +340,7 @@ class Authenticator(Module): # pylint: disable=too-many-public-methods
"""
return True
def transformUsername(self, username: str) -> str:
def transformUsername(self, username: str, request: 'ExtendedHttpRequest') -> str:
"""
On login, this method get called so we can "transform" provided user name.
@ -356,7 +356,7 @@ class Authenticator(Module): # pylint: disable=too-many-public-methods
return username
def internalAuthenticate(
self, username: str, credentials: str, groupsManager: 'GroupsManager'
self, username: str, credentials: str, groupsManager: 'GroupsManager', request: 'ExtendedHttpRequest'
) -> bool:
"""
This method is provided so "plugins" (For example, a custom dispatcher), can test
@ -391,7 +391,7 @@ class Authenticator(Module): # pylint: disable=too-many-public-methods
This is done in this way, because UDS has only a subset of groups for this user, and
we let the authenticator decide inside wich groups of UDS this users is included.
"""
return self.authenticate(username, credentials, groupsManager)
return self.authenticate(username, credentials, groupsManager, request)
def logout(self, username: str) -> typing.Optional[str]:
"""
@ -486,7 +486,7 @@ class Authenticator(Module): # pylint: disable=too-many-public-methods
self,
parameters: typing.Dict[str, typing.Any],
gm: 'GroupsManager',
request: 'ExtendedHttpRequestWithUser',
request: 'ExtendedHttpRequest',
) -> typing.Optional[str]:
"""
There is a view inside UDS, an url, that will redirect the petition

View File

@ -39,14 +39,12 @@ from django.utils import timezone
from uds.core.util import os_detector as OsDetector
from uds.core.util.config import GlobalConfig
from uds.core.auths.auth import EXPIRY_KEY, ROOT_ID, USER_KEY, getRootUser, webLogout
from uds.core.util.request import (
setRequest,
delCurrentRequest,
cleanOldRequests,
ExtendedHttpRequest,
)
from uds.models import User
if typing.TYPE_CHECKING:
from uds.core.util.request import ExtendedHttpRequest
logger = logging.getLogger(__name__)
# How often to check the requests cache for stuck objects
@ -59,22 +57,16 @@ class GlobalRequestMiddleware:
def __init__(self, get_response: typing.Callable[[HttpRequest], HttpResponse]):
self._get_response: typing.Callable[[HttpRequest], HttpResponse] = get_response
def _process_response(self, request: ExtendedHttpRequest, response: HttpResponse):
def _process_response(self, request: 'ExtendedHttpRequest', response: HttpResponse):
# Remove IP from global cache (processing responses after this will make global request unavailable,
# but can be got from request again)
delCurrentRequest()
# Clean old stored if needed
GlobalRequestMiddleware.cleanStuckRequests()
return response
def __call__(self, request: ExtendedHttpRequest):
def __call__(self, request: 'ExtendedHttpRequest'):
# Add IP to request
GlobalRequestMiddleware.fillIps(request)
# Store request on cache
setRequest(request=request)
# Ensures request contains os
request.os = OsDetector.getOsFromUA(
request.META.get('HTTP_USER_AGENT', 'Unknown')
@ -105,17 +97,7 @@ class GlobalRequestMiddleware:
return self._process_response(request, response)
@staticmethod
def cleanStuckRequests() -> None:
# In case of some exception, keep clean very old request from time to time...
if (
GlobalRequestMiddleware.lastCheck
> datetime.datetime.now() - datetime.timedelta(seconds=CHECK_SECONDS)
):
return
cleanOldRequests()
@staticmethod
def fillIps(request: ExtendedHttpRequest):
def fillIps(request: 'ExtendedHttpRequest'):
"""
Obtains the IP of a Django Request, even behind a proxy
@ -161,7 +143,7 @@ class GlobalRequestMiddleware:
logger.debug('ip: %s, ip_proxy: %s', request.ip, request.ip_proxy)
@staticmethod
def getUser(request: ExtendedHttpRequest) -> None:
def getUser(request: 'ExtendedHttpRequest') -> None:
"""
Ensures request user is the correct user
"""

View File

@ -59,69 +59,3 @@ class ExtendedHttpRequest(HttpRequest):
class ExtendedHttpRequestWithUser(ExtendedHttpRequest):
user: User
identity_context: contextvars.ContextVar[int] = contextvars.ContextVar('identity')
# Return an unique id for the current running thread or the current running coroutine
def getIdent() -> int:
# Defect if we are on a thread or on asyncio
try:
if asyncio.get_event_loop().is_running():
if identity_context.get(None) is None:
identity_context.set(
# Generate a really unique random number for the asyncio task based on current time
# lower 16 are random, upper bits are based on current time
random.randint(0, 2 ** 16 - 1)
+ int(datetime.datetime.now().timestamp()) * 2 ** 16
) # Every "task" has its own context
return identity_context.get()
except Exception:
pass
return threading.current_thread().ident or -1
def getRequest() -> ExtendedHttpRequest:
ident = getIdent()
val = (
typing.cast(typing.Optional[ExtendedHttpRequest], _requests[ident][0]())
if ident in _requests
else None
) # Return obj from weakref
return val or ExtendedHttpRequest()
def delCurrentRequest() -> None:
ident = getIdent()
logger.debug('Deleting %s', ident)
try:
if ident in _requests:
del _requests[ident] # Remove stored request
else:
logger.info('Request id %s not stored in cache', ident)
except Exception:
logger.exception('Deleting stored request')
def cleanOldRequests() -> None:
logger.debug('Cleaning stuck requests from %s', _requests)
# No request lives 3600 seconds, so 3600 seconds is fine
cleanFrom: datetime.datetime = datetime.datetime.now() - datetime.timedelta(
seconds=3600
)
toDelete: typing.List[int] = []
for ident, request in _requests.items():
if request[1] < cleanFrom:
toDelete.append(ident)
for ident in toDelete:
try:
del _requests[ident]
except Exception:
pass # Ignore it silently
def setRequest(request: ExtendedHttpRequest):
_requests[getIdent()] = (
weakref.ref(typing.cast(ExtendedHttpRequest, request)),
datetime.datetime.now(),
)

View File

@ -124,7 +124,7 @@ def checkLogin( # pylint: disable=too-many-branches, too-many-statements
user = None
if password == '':
password = 'axd56adhg466jasd6q8sadñ€sáé--v' # Random string, in fact, just a placeholder that will not be used :)
user = authenticate(userName, password, authenticator)
user = authenticate(userName, password, authenticator, request=request)
logger.debug('User: %s', user)
if user is None: