diff --git a/server/src/uds/REST/methods/authenticators.py b/server/src/uds/REST/methods/authenticators.py index 40e03d0e..28dc4ddb 100644 --- a/server/src/uds/REST/methods/authenticators.py +++ b/server/src/uds/REST/methods/authenticators.py @@ -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) diff --git a/server/src/uds/REST/model.py b/server/src/uds/REST/model.py index 812df3e8..25de5102 100644 --- a/server/src/uds/REST/model.py +++ b/server/src/uds/REST/model.py @@ -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': _( diff --git a/server/src/uds/auths/IP/authenticator.py b/server/src/uds/auths/IP/authenticator.py index c45d8eac..91837edb 100644 --- a/server/src/uds/auths/IP/authenticator.py +++ b/server/src/uds/auths/IP/authenticator.py @@ -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, diff --git a/server/src/uds/core/auths/authenticator.py b/server/src/uds/core/auths/authenticator.py index 38951be7..7b01742b 100644 --- a/server/src/uds/core/auths/authenticator.py +++ b/server/src/uds/core/auths/authenticator.py @@ -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 diff --git a/server/src/uds/migrations/0044_auto_20211122_1207.py b/server/src/uds/migrations/0044_auto_20211122_1207.py index cf625ed9..d8e72aa8 100644 --- a/server/src/uds/migrations/0044_auto_20211122_1207.py +++ b/server/src/uds/migrations/0044_auto_20211122_1207.py @@ -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, ), diff --git a/server/src/uds/models/authenticator.py b/server/src/uds/models/authenticator.py index 8adadc54..23992eec 100644 --- a/server/src/uds/models/authenticator.py +++ b/server/src/uds/models/authenticator.py @@ -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 diff --git a/server/src/uds/models/transport.py b/server/src/uds/models/transport.py index da39d024..8bfca78f 100644 --- a/server/src/uds/models/transport.py +++ b/server/src/uds/models/transport.py @@ -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 diff --git a/server/src/uds/web/util/authentication.py b/server/src/uds/web/util/authentication.py index e904f887..9da99b8b 100644 --- a/server/src/uds/web/util/authentication.py +++ b/server/src/uds/web/util/authentication.py @@ -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, diff --git a/server/src/uds/web/util/configjs.py b/server/src/uds/web/util/configjs.py index b439c8a6..ebe08e50 100644 --- a/server/src/uds/web/util/configjs.py +++ b/server/src/uds/web/util/configjs.py @@ -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 diff --git a/server/src/uds/web/views/modern.py b/server/src/uds/web/views/modern.py index 8f49adb2..3089a51c 100644 --- a/server/src/uds/web/views/modern.py +++ b/server/src/uds/web/views/modern.py @@ -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))