forked from shaba/openuds
Completed network filtering for authentication, and improbed network filtering on transports (better undestanding now)
This commit is contained in:
parent
e8dae69f6f
commit
057a26ea7b
@ -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)
|
||||
|
@ -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': _(
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
||||
|
@ -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))
|
||||
|
Loading…
Reference in New Issue
Block a user