Completed network filtering for authentication, and improbed network filtering on transports (better undestanding now)

This commit is contained in:
Adolfo Gómez García 2021-11-23 11:55:55 +01:00
parent e8dae69f6f
commit 057a26ea7b
10 changed files with 92 additions and 42 deletions

View File

@ -57,7 +57,17 @@ class Authenticators(ModelHandler):
# Custom get method "search" that requires authenticator id
custom_methods = [('search', True)]
detail = {'users': Users, 'groups': Groups}
save_fields = ['name', 'comments', 'tags', 'priority', 'small_name', 'state']
# Networks is treated on "beforeSave", so it is not include on "save_fields" because it is not
# automatically included in the "save" method.
save_fields = [
'name',
'comments',
'tags',
'priority',
'small_name',
'net_filtering',
'state',
]
table_title = _('Authenticators')
table_fields = [
@ -70,7 +80,7 @@ class Authenticators(ModelHandler):
'state': {
'title': _('Access'),
'type': 'dict',
'dict': {'x': _('Visible'), 'h': _('Hidden'), 'd': 'Disabled'},
'dict': {'v': _('Visible'), 'h': _('Hidden'), 'd': 'Disabled'},
'width': '3em',
}
},
@ -149,6 +159,19 @@ class Authenticators(ModelHandler):
'permission': permissions.getEffectivePermission(self._user, item),
}
def afterSave(self, item: Authenticator) -> None:
try:
networks = self._params['networks']
except Exception: # No networks passed in, this is ok
logger.debug('No networks')
return
if (
networks is None
): # None is not provided, empty list is ok and means no networks
return
logger.debug('Networks: %s', networks)
item.networks.set(Network.objects.filter(uuid__in=networks)) # type: ignore # set is not part of "queryset"
# Custom "search" method
def search(self, item: Authenticator) -> typing.List[typing.Dict]:
self.ensureAccess(item, permissions.PERMISSION_READ)

View File

@ -210,11 +210,11 @@ class BaseModelHandler(Handler):
gui,
{
'name': 'net_filtering',
'value': 'x',
'value': 'n',
'values': [
{'id': 'x', 'text': _('Disabled')},
{'id': 'a', 'text': _('Allow')},
{'id': 'd', 'text': _('Deny')},
{'id': 'n', 'text': _('No filtering')},
{'id': 'a', 'text': _('Allow selected networks')},
{'id': 'd', 'text': _('Deny selected networks')},
],
'label': _('Network Filtering'),
'tooltip': _(

View File

@ -107,7 +107,7 @@ class IPAuth(auths.Authenticator):
return True
return False
def isVisibleFrom(self, request: 'ExtendedHttpRequest'):
def isAccesibleFrom(self, request: 'ExtendedHttpRequest'):
"""
Used by the login interface to determine if the authenticator is visible on the login page.
"""
@ -115,7 +115,7 @@ class IPAuth(auths.Authenticator):
# If has networks and not in any of them, not visible
if validNets and not net.ipInNetwork(request.ip, validNets):
return False
return super().isVisibleFrom(request)
return super().isAccesibleFrom(request)
def internalAuthenticate(
self,

View File

@ -292,7 +292,11 @@ class Authenticator(Module): # pylint: disable=too-many-public-methods
return []
def authenticate(
self, username: str, credentials: str, groupsManager: 'GroupsManager', request: 'ExtendedHttpRequest'
self,
username: str,
credentials: str,
groupsManager: 'GroupsManager',
request: 'ExtendedHttpRequest',
) -> bool:
"""
This method must be overriden, and is responsible for authenticating
@ -334,16 +338,16 @@ class Authenticator(Module): # pylint: disable=too-many-public-methods
"""
return False
def isVisibleFrom(self, request: 'HttpRequest'):
def isAccesibleFrom(self, request: 'HttpRequest'):
"""
Used by the login interface to determine if the authenticator is visible on the login page.
"""
from uds.core.util.request import ExtendedHttpRequest
from uds.models import Authenticator as dbAuth
if isinstance(request, ExtendedHttpRequest):
return self._dbAuth.validForIp(request.ip)
return self._dbAuth.state == dbAuth.VISIBLE
return self._dbAuth.state != dbAuth.DISABLED and self._dbAuth.validForIp(
typing.cast('ExtendedHttpRequest', request).ip
)
def transformUsername(self, username: str, request: 'ExtendedHttpRequest') -> str:
"""
@ -361,7 +365,11 @@ class Authenticator(Module): # pylint: disable=too-many-public-methods
return username
def internalAuthenticate(
self, username: str, credentials: str, groupsManager: 'GroupsManager', request: 'ExtendedHttpRequest'
self,
username: str,
credentials: str,
groupsManager: 'GroupsManager',
request: 'ExtendedHttpRequest',
) -> bool:
"""
This method is provided so "plugins" (For example, a custom dispatcher), can test

View File

@ -7,11 +7,12 @@ from uds.models import Transport, Authenticator
TRANS_ALLOW = Transport.ALLOW
TRANS_DENY = Transport.DENY
TRANS_DISABLED = Transport.DISABLED
TRANS_NOFILTERING = Transport.NO_FILTERING
# Auths
AUTH_ALLOW = Authenticator.ALLOW
AUTH_DENY = Authenticator.DENY
AUTH_NOFILTERING = Authenticator.NO_FILTERING
AUTH_DISABLED = Authenticator.DISABLED
AUTH_VISIBLE = Authenticator.VISIBLE
AUTH_HIDDEN = Authenticator.HIDDEN
@ -19,7 +20,7 @@ AUTH_HIDDEN = Authenticator.HIDDEN
def migrate_fwd(apps, schema_editor):
Transport = apps.get_model('uds', 'Transport')
for transport in Transport.objects.all():
value = TRANS_DISABLED # Defaults to "not configured"
value = TRANS_NOFILTERING # Defaults to "not configured"
if transport.networks.count() > 0:
if transport.nets_positive:
value = TRANS_ALLOW
@ -66,7 +67,7 @@ class Migration(migrations.Migration):
model_name='authenticator',
name='net_filtering',
field=models.CharField(
default=AUTH_DISABLED,
default=AUTH_NOFILTERING,
max_length=1,
db_index=True,
),
@ -84,7 +85,7 @@ class Migration(migrations.Migration):
model_name='transport',
name='net_filtering',
field=models.CharField(
default=TRANS_DISABLED,
default=TRANS_NOFILTERING,
max_length=1,
db_index=True,
),

View File

@ -60,11 +60,12 @@ class Authenticator(ManagedObjectModel, TaggingMixin):
# Constants for Visibility
VISIBLE = 'v'
HIDDEN = 'h'
# Visibility and net_filter
DISABLED = 'd'
# net_filter
# Note: this are STANDARD values used on "default field" networks on RESP API
# Named them for better reading, but cannot be changed, since they are used on RESP API
NO_FILTERING = 'n'
ALLOW = 'a'
DENY = 'd'
@ -72,7 +73,7 @@ class Authenticator(ManagedObjectModel, TaggingMixin):
small_name = models.CharField(max_length=32, default='', db_index=True)
state = models.CharField(max_length=1, default=VISIBLE, db_index=True)
# visible = models.BooleanField(default=True)
net_filtering = models.CharField(max_length=1, default=DISABLED, db_index=True)
net_filtering = models.CharField(max_length=1, default=NO_FILTERING, db_index=True)
# "fake" relations declarations for type checking
objects: 'models.BaseManager[Authenticator]'
@ -212,18 +213,18 @@ class Authenticator(ManagedObjectModel, TaggingMixin):
False if the ip can't access this Transport.
The check is done using the net_filtering field.
if net_filtering is 'x' (disabled), then the result is always True
if net_filtering is 'd' (disabled), then the result is always True
if net_filtering is 'a' (allow), then the result is True is the ip is in the networks
if net_filtering is 'd' (deny), then the result is True is the ip is not in the networks
Raises:
:note: Ip addresses has been only tested with IPv4 addresses
"""
if self.net_filtering == 'x':
if self.net_filtering == Authenticator.NO_FILTERING:
return True
ip = net.ipToLong(ipStr)
# Allow
if self.net_filtering == 'a':
if self.net_filtering == Authenticator.ALLOW:
return self.networks.filter(net_start__lte=ip, net_end__gte=ip).exists()
# Deny, must not be in any network
return self.networks.filter(net_start__lte=ip, net_end__gte=ip).exists() is False

View File

@ -57,13 +57,13 @@ class Transport(ManagedObjectModel, TaggingMixin):
Sample of transports are RDP, Spice, Web file uploader, etc...
"""
# Constants for net_filter
DISABLED = 'd'
NO_FILTERING = 'n'
ALLOW = 'a'
DENY = 'x'
DENY = 'd'
# pylint: disable=model-missing-unicode
priority = models.IntegerField(default=0, db_index=True)
net_filtering = models.CharField(max_length=1, default=DISABLED, db_index=True)
net_filtering = models.CharField(max_length=1, default=NO_FILTERING, db_index=True)
# We store allowed oss as a comma-separated list
allowed_oss = models.CharField(max_length=255, default='')
# Label, to group transports on meta pools
@ -124,11 +124,11 @@ class Transport(ManagedObjectModel, TaggingMixin):
:note: Ip addresses has been only tested with IPv4 addresses
"""
if self.net_filtering == 'x':
if self.net_filtering == Transport.NO_FILTERING:
return True
ip = net.ipToLong(ipStr)
# Allow
if self.net_filtering == 'a':
if self.net_filtering == Transport.ALLOW:
return self.networks.filter(net_start__lte=ip, net_end__gte=ip).exists()
# Deny, must not be in any network
return self.networks.filter(net_start__lte=ip, net_end__gte=ip).exists() is False

View File

@ -111,7 +111,7 @@ def checkLogin( # pylint: disable=too-many-branches, too-many-statements
_('Too many authentication errrors. User temporarily blocked'),
)
# check if authenticator is visible for this requests
if authInstance.isVisibleFrom(request=request) is False:
if authInstance.isAccesibleFrom(request=request) is False:
authLogLogin(
request,
authenticator,

View File

@ -59,7 +59,6 @@ register = template.Library()
CSRF_FIELD = 'csrfmiddlewaretoken'
@register.simple_tag(takes_context=True)
def udsJs(request: 'ExtendedHttpRequest') -> str:
auth_host = (
request.META.get('HTTP_HOST') or request.META.get('SERVER_NAME') or 'auth_host'
@ -91,28 +90,34 @@ def udsJs(request: 'ExtendedHttpRequest') -> str:
tag = request.session.get('tag', None)
logger.debug('Tag config: %s', tag)
# Initial list of authenticators (all except disabled ones)
auths = Authenticator.objects.exclude(state=Authenticator.DISABLED)
authenticators: typing.List[Authenticator] = []
if GlobalConfig.DISALLOW_GLOBAL_LOGIN.getBool():
try:
# Get authenticators with auth_host or tag. If tag is None, auth_host, if exists
# Tag will also include non visible authenticators
# tag, later will remove "auth_host"
authenticators = list(auths.filter(
small_name__in=[auth_host, tag]
))
authenticators = list(auths.filter(small_name__in=[auth_host, tag]))
except Exception as e:
authenticators = []
else:
authenticators = list(auths)
if not tag: # If no tag, remove hidden auths
auths = auths.filter(state=Authenticator.VISIBLE)
authenticators = list(
auths
)
# Filter out non visible authenticators (using origin and visible field right now)
authenticators = [a for a in authenticators if a.getInstance().isVisibleFrom(request)]
# Filter out non accesible authenticators (using origin)
authenticators = [
a for a in authenticators if a.getInstance().isAccesibleFrom(request)
]
# logger.debug('Authenticators PRE: %s', authenticators)
if (
tag and authenticators
): # Refilter authenticators, visible and with this tag if required
): # Refilter authenticators, not disabled and with this tag if required
authenticators = [
x
for x in authenticators
@ -120,12 +125,19 @@ def udsJs(request: 'ExtendedHttpRequest') -> str:
or (tag == 'disabled' and x.getType().isCustom() is False)
]
# No autenticator can reach the criteria, let's do a final try
# disabled mean "does not use any specific auth, just the root one"
if not authenticators and tag != 'disabled':
try:
authenticators = [Authenticator.objects.order_by('priority')[0]]
except Exception: # There is no authenticators yet...
authenticators = []
for a in Authenticator.objects.exclude(state=Authenticator.DISABLED).order_by('priority'):
if a.getInstance().isAccesibleFrom(request):
authenticators.append(a)
break
except Exception:
authenticators = []
# No tag, and there are authenticators, let's use the first one
if not tag and authenticators:
tag = authenticators[0].small_name

View File

@ -34,6 +34,7 @@ import typing
from django.shortcuts import render
from django.http import HttpRequest, HttpResponse, JsonResponse, HttpResponseRedirect
from django.views.decorators.cache import never_cache
from django.urls import reverse
from uds.core.util.request import ExtendedHttpRequest, ExtendedHttpRequestWithUser
from uds.core.auths import auth
@ -47,7 +48,7 @@ from uds.web.util import configjs
logger = logging.getLogger(__name__)
@never_cache
def index(request: HttpRequest) -> HttpResponse:
# return errorView(request, 1)
response = render(request, 'uds/modern/index.html', {})
@ -59,12 +60,14 @@ def index(request: HttpRequest) -> HttpResponse:
# Includes a request.session ticket, indicating that
@never_cache
def ticketLauncher(request: HttpRequest) -> HttpResponse:
request.session['restricted'] = True # Access is from ticket
return index(request)
# Basically, the original /login method, but fixed for modern interface
@never_cache
def login(
request: ExtendedHttpRequest, tag: typing.Optional[str] = None
) -> HttpResponse:
@ -97,6 +100,7 @@ def login(
return response
@never_cache
@auth.webLoginRequired(admin=False)
def logout(request: ExtendedHttpRequestWithUser) -> HttpResponse:
auth.authLogLogout(request)
@ -106,13 +110,14 @@ def logout(request: ExtendedHttpRequestWithUser) -> HttpResponse:
logoutUrl = request.session.get('logouturl', None)
return auth.webLogout(request, logoutUrl)
@never_cache
def js(request: ExtendedHttpRequest) -> HttpResponse:
return HttpResponse(
content=configjs.udsJs(request), content_type='application/javascript'
)
@never_cache
@auth.denyNonAuthenticated
def servicesData(request: ExtendedHttpRequestWithUser) -> HttpResponse:
return JsonResponse(getServicesData(request))