Source code for x2gobroker.brokers.base_broker

# -*- coding: utf-8 -*-
# vim:fenc=utf-8

# Copyright (C) 2012-2020 by Mike Gabriel <mike.gabriel@das-netzwerkteam.de>
# Copyright (C) 2012-2020 by Josh Lukens <jlukens@botch.com>
#
# X2Go Session Broker is free software; you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# X2Go Session Broker is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program; if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.

"""\
:class:`x2gobroker.brokers.base_broker.X2GoBroker` class - base skeleton for X2GoBroker implementations

"""
__NAME__ = 'x2gobroker-pylib'

# modules
import copy
import socket
import uuid
import netaddr
import random
import time
import os.path

# X2Go Broker modules
import x2gobroker.config
import x2gobroker.defaults
import x2gobroker.agent
import x2gobroker.x2gobroker_exceptions
import x2gobroker.loadchecker

from x2gobroker.loggers import logger_broker, logger_error

from x2gobroker.defaults import X2GOBROKER_USER as _X2GOBROKER_USER
from x2gobroker.defaults import X2GOBROKER_DAEMON_USER as _X2GOBROKER_DAEMON_USER

[docs] class X2GoBroker(object): """\ :class:`x2gobroker.brokers.base_broker.X2GoBroker` is an abstract class for X2Go broker implementations. This class needs to be inherited from a concrete broker class. Currently available broker classes are:: :class:`zeroconf.X2GoBroker <x2gobroker.brokers.zeroconf.X2GoBroker>` (working) :class:`inifile.X2GoBroker <x2gobroker.brokers.inifile.X2GoBroker>` (working) :class:`ldap.X2GoBroker <x2gobroker.brokers.ldap.X2GoBroker>` (in prep) """ backend_name = 'base' nameservice_module = None authmech_module = None def __init__(self, config_file=None, config_defaults=None): """\ Initialize a new X2GoBroker instance to control X2Go session through an X2Go Client with an intermediate session broker. :param config_file: path to the X2Go Session Broker configuration file (x2gobroker.conf) :type config_file: ``str`` :param config_defaults: Default settings for the broker's global configuration parameters. :type config_defaults: ``dict`` """ self.config_file = config_file if self.config_file is None: self.config_file = x2gobroker.defaults.X2GOBROKER_CONFIG if config_defaults is None: config_defaults = x2gobroker.defaults.X2GOBROKER_CONFIG_DEFAULTS self.config = x2gobroker.config.X2GoBrokerConfigFile(config_files=self.config_file, defaults=config_defaults) self.enabled = self.config.get_value('broker_{backend}'.format(backend=self.backend_name), 'enable') self._dynamic_cookie_map = {} self._client_address = None self._cookie = None def __del__(self): """\ Cleanup on destruction of an :class:`X2GoBroker <x2gobroker.brokers.base_broker.X2GoBroker>` instance. """ pass
[docs] def is_enabled(self): """\ Check if this backend has been enabled in the configuration file. """ return self.enabled
[docs] def get_name(self): """\ Accessor for self.backend_name property. :returns: the backend name :rtype: ``str`` """ return self.backend_name
[docs] def enable(self): """\ Enable this broker backend. """ self.enabled = True
[docs] def disable(self): """\ Disable this broker backend. """ self.enabled = False
[docs] def set_client_address(self, address): """\ Set the client IP address. :param address: the client IP :type address: ``str`` """ if netaddr.valid_ipv6(address): pass elif netaddr.valid_ipv4(address): pass else: self._client_address = None raise ValueError('address {address} is neither a valid IPv6 nor a valid IPv4 address'.format(address=address)) self._client_address = netaddr.IPAddress(address)
[docs] def get_client_address(self): """\ Get the client IP address (if set). :returns: the client IP (either IPv4 or IPv6) :rtype: ``str`` """ if self._client_address: return str(self._client_address) else: return None
[docs] def get_client_address_type(self): """\ Get the client IP address type of the client address (if set). :returns: the client address type (4: IPv4, 6: IPv6) :rtype: ``int`` """ return self._client_address.version
[docs] def get_global_config(self): """\ Get the global section of the configuration file. :returns: all global configuration parameters :rtype: ``dict`` """ return self.config.get_section('global')
[docs] def get_global_value(self, option): """\ Get the configuration setting for an option in the global section of the configuration file. :param option: option name in the global configuration section :type option: ``str`` :returns: the value for the given global ``option`` :rtype: ``bool``, ``str``, ``int`` or ``list`` """ return self.config.get_value('global', option)
[docs] def get_backend_config(self): """\ Get the configuration section of a specific backend. :returns: all backend configuration parameters :rtype: ``dict`` """ return self.config.get_section('broker_{backend}'.format(backend=self.backend_name))
[docs] def get_backend_value(self, backend='zeroconf', option='enable'): """\ Get the configuration setting for backend ``backend`` and option ``option``. :param backend: the name of the backend :type backend: ``str`` :param option: option name of the backend's configuration section :type option: ``str`` :returns: the value for the given ``backend`` ``option`` :rtype: ``bool``, ``str``, ``int`` or ``list`` """ return self.config.get_value(backend, option)
[docs] def get_profile_ids(self): """\ Retrieve the complete list of session profile IDs. :returns: list of profile IDs :rtype: ``list`` """ return []
[docs] def get_profile_ids_for_user(self, username): """\ Retrieve the list of session profile IDs for a given user. :param username: query profile id list for this user :type username: ``str`` :returns: list of profile IDs :rtype: ``list`` """ return [ id for id in self.get_profile_ids() if self.check_profile_acls(username, self.get_profile_acls(id)) ]
[docs] def get_profile_defaults(self): """\ Get the session profile defaults, i.e. profile options that all configured session profiles have in common. The defaults are hard-coded in :mod:`x2gobroker.defaults` for class :class:`x2gobroker.brokers.base_broker.X2GoBroker`. :returns: a dictionary containing the session profile defaults :rtype: ``dict`` """ profile_defaults = copy.deepcopy(x2gobroker.defaults.X2GOBROKER_SESSIONPROFILE_DEFAULTS['DEFAULT']) for key in copy.deepcopy(profile_defaults): if key.startswith('acl-'): del profile_defaults[key] return profile_defaults
[docs] def get_acl_defaults(self): """\ Get the ACL defaults for session profiles. The defaults are hard-coded in :mod:`x2gobroker.defaults` for class :class:`x2gobroker.brokers.base_broker.X2GoBroker`. :returns: a dictionary containing the ACL defaults for all session profiles :rtype: ``dict`` """ acl_defaults = copy.deepcopy(x2gobroker.defaults.X2GOBROKER_SESSIONPROFILE_DEFAULTS['DEFAULT']) for key in copy.deepcopy(acl_defaults): if not key.startswith('acl-'): del acl_defaults[key] return acl_defaults
[docs] def get_profile(self, profile_id): """\ Get the session profile for profile ID <profile_id>. :param profile_id: the ID of a profile :type profile_id: ``str`` :returns: a dictionary representing the session profile for ID <profile_id> :rtype: ``dict`` """ return {}
[docs] def get_profile_broker(self, profile_id): """\ Get broker-specific session profile options from the session profile with profile ID <profile_id>. :param profile_id: the ID of a profile :type profile_id: ``str`` :returns: a dictionary representing the session profile for ID <profile_id> :rtype: ``dict`` """ return {}
[docs] def get_profile_acls(self, profile_id): """\ Get the ACLs for session profile with profile ID <profile_id>. :param profile_id: the ID of a profile :type profile_id: ``str`` :returns: a dictionary representing the ACLs for session profile with ID <profile_id> :rtype: ``dict`` """ return {}
[docs] def check_profile_acls(self, username, acls): """\ Test if a given user can get through an ACL check using <acls> as a list of allow and deny rules. :param username: the username of interest :type username: ``str`` :param acls: a dictionary data structure containing ACL information (see :envvar:`x2gobroker.defaults.X2GOBROKER_SESSIONPROFILE_DEFAULTS`) :type acls: ``dict`` """ ### extract ACLs evaluation orders _acls = self.get_acl_defaults() _acls.update(acls) _order = {} _order['users'] = _order['groups'] = _order['clients'] = _acls['acl-any-order'] try: _order['users'] = _acls['acl-users-order'] except KeyError: pass try: _order['groups'] = _acls['acl-groups-order'] except KeyError: pass try: _order['clients'] = _acls['acl-clients-order'] except KeyError: pass # to pass an ACL test, all three keys in the dict below have to be set to True # if one stays False, the related session profile will not be returned to the querying # X2Go client... _grant_availability = { 'by_user': None, 'by_group': None, 'by_client': None, } ### CHECKING on a per-client basis... ### clients access is granted first, if that fails then we return False here... if len( _acls['acl-clients-allow'] + _acls['acl-clients-deny'] ) > 0: _acls_clients_allow = copy.deepcopy(_acls['acl-clients-allow']) _acls_clients_deny = copy.deepcopy(_acls['acl-clients-deny']) _allow_client = False _deny_client = False for idx, item in enumerate(_acls_clients_allow): if item == 'ALL': _acls_clients_allow[idx] = '0.0.0.0/0' _acls_clients_allow.insert(idx, '::/0') for idx, item in enumerate(_acls_clients_deny): if item == 'ALL': _acls_clients_deny[idx] = '0.0.0.0/0' _acls_clients_deny.insert(idx, '::/0') _allow_address_set = [] _deny_address_set = [] try: _allow_address_set = netaddr.IPSet(_acls_clients_allow) _deny_address_set = netaddr.IPSet(_acls_clients_deny) except netaddr.core.AddrFormatError as e: logger_error.error('base_broker.X2GoBroker.check_acls(): netaddr.core.AddrFormatError - {why}'.format(why=str(e))) except ValueError as e: logger_error.error('base_broker.X2GoBroker.check_acls(): ValueError - {why}'.format(why=str(e))) _allow_client = self._client_address in _allow_address_set _deny_client = self._client_address in _deny_address_set if _order['clients'] == 'allow-deny': if _allow_client: _grant_availability['by_client'] = True elif _deny_client : _grant_availability['by_client'] = False else: if _deny_client : _grant_availability['by_client'] = False elif _allow_client: _grant_availability['by_client'] = True if _grant_availability['by_client'] is not True: return False ### no user/group ACLs are in use, allow access then... if len(_acls['acl-users-allow'] + _acls['acl-users-deny'] + _acls['acl-groups-allow'] + _acls['acl-groups-deny']) == 0: return True ### CHECKING on a per-user basis... if len( _acls['acl-users-allow'] + _acls['acl-users-deny'] ) > 0: _allow_user = False _deny_user = False if username in _acls['acl-users-allow'] or 'ALL' in _acls['acl-users-allow']: _allow_user = True if username in _acls['acl-users-deny'] or 'ALL' in _acls['acl-users-deny']: _deny_user = True if _order['users'] == 'allow-deny': if _allow_user: _grant_availability['by_user'] = True elif _deny_user : _grant_availability['by_user'] = False else: if _deny_user : _grant_availability['by_user'] = False elif _allow_user: _grant_availability['by_user'] = True # if a user has been granted access directly, then the corresponding session profile(s) # will be provided to him/her, it does not matter what the group acl will have to say to this... if _grant_availability['by_user']: return True ### CHECKING on a per-group basis... if len(_acls['acl-groups-allow'] + _acls['acl-groups-deny']) > 0: _allow_group = False _deny_group = False _user_groups = ['ALL'] + self.get_user_groups(username, primary_groups=not self.get_global_value('ignore-primary-group-memberships')) _allow_group = bool(len(set(_user_groups).intersection( set(_acls['acl-groups-allow']) ))) _deny_group = bool(len(set(_user_groups).intersection( set(_acls['acl-groups-deny']) ))) if _order['groups'] == 'allow-deny': if _allow_group: _grant_availability['by_group'] = True elif _deny_group : _grant_availability['by_group'] = False else: if _deny_group : _grant_availability['by_group'] = False elif _allow_group: _grant_availability['by_group'] = True if _grant_availability['by_group'] and _grant_availability['by_user'] is not False: return True return False
[docs] def test_connection(self): return 'OK'
def _import_authmech_module(self, mech='pam'): try: if self.authmech_module is None: _authmech_module = None namespace = {} exec("import x2gobroker.authmechs.{mech}_authmech as _authmech_module".format(mech=mech), namespace) self.authmech_module = namespace['_authmech_module'] return True except ImportError: return False def _do_authenticate(self, username='', password=''): mech = self.get_authentication_mechanism() logger_broker.debug('base_broker.X2GoBroker._do_authenticate(): attempting authentication, will try "{mech}" mechanism for authenticating the user.'.format(mech=mech)) if self._import_authmech_module(mech=mech): logger_broker.debug('base_broker.X2GoBroker._do_authenticate(): authenticating user={username} with password=<hidden> against mechanism "{mech}".'.format(username=username, mech=mech)) return self.authmech_module.X2GoBrokerAuthMech().authenticate(username, password, config=self.config) else: return False
[docs] def get_authentication_mechanism(self): """\ Get the name of the authentication mechanism that is configured for this X2Go Session Broker instance. :returns: auth-mech name :rtype: ``str`` """ _default_auth_mech = "pam" _auth_mech = "" if self.config.has_value('broker_{backend}'.format(backend=self.backend_name), 'auth-mech') and self.config.get_value('broker_{backend}'.format(backend=self.backend_name), 'auth-mech'): _auth_mech = self.config.get_value('broker_{backend}'.format(backend=self.backend_name), 'auth-mech').lower() logger_broker.debug('base_broker.X2GoBroker.get_authentication_mechanism(): found auth-mech in backend config section »{backend}«: {value}. This one has precendence over the default value.'.format(backend=self.backend_name, value=_auth_mech)) elif self.config.has_value('global', 'default-auth-mech'): _default_auth_mech = self.config.get_value('global', 'default-auth-mech').lower() logger_broker.debug('base_broker.X2GoBroker.get_authentication_mechanism(): found default-auth-mech in global config section: {value}'.format(value=_default_auth_mech)) return _auth_mech or _default_auth_mech
def _enforce_agent_query_mode(self, mode='LOCAL'): """\ Allow frontends to enforce a certain broker agent backend. :param mode: what agent query mode demanded :type mode: ``str`` :returns: the agent query mode we force the broker to :rtype: ``str`` """ return None
[docs] def get_agent_query_mode(self, profile_id): """\ Get the agent query mode (LOCAL or SSH, normally) that is configured for this X2Go Session Broker instance. :returns: agent query mode :rtype: ``str`` """ _default_agent_query_mode = "LOCAL" _backend_agent_query_mode = "" _agent_query_mode = "" _profile = self.get_profile_broker(profile_id) if _profile and 'broker-agent-query-mode' in _profile and _profile['broker-agent-query-mode']: _agent_query_mode = _profile['broker-agent-query-mode'] logger_broker.debug('base_broker.X2GoBroker.get_agent_query_mode(): found broker-agent-query-mode in session profile with ID {id}: {value}. This one has precendence over the default and the backend value.'.format(id=profile_id, value=_agent_query_mode)) elif self.config.has_value('broker_{backend}'.format(backend=self.backend_name), 'agent-query-mode') and self.config.get_value('broker_{backend}'.format(backend=self.backend_name), 'agent-query-mode'): _backend_agent_query_mode = self.config.get_value('broker_{backend}'.format(backend=self.backend_name), 'agent-query-mode').lower() logger_broker.debug('base_broker.X2GoBroker.get_agent_query_mode(): found agent-query-mode in backend config section »{backend}«: {value}. This one has precendence over the default value.'.format(backend=self.backend_name, value=_agent_query_mode)) elif self.config.has_value('global', 'default-agent-query-mode') and self.config.get_value('global', 'default-agent-query-mode'): _default_agent_query_mode = self.config.get_value('global', 'default-agent-query-mode').lower() logger_broker.debug('base_broker.X2GoBroker.get_agent_query_mode(): found default-agent-query-mode in global config section: {value}'.format(value=_default_agent_query_mode)) _mode = _agent_query_mode or _backend_agent_query_mode or _default_agent_query_mode # if the frontend overrides the agent query mode, immediately return it here... if self._enforce_agent_query_mode(mode=_mode): _new_mode = self._enforce_agent_query_mode(mode=_mode) logger_broker.debug('base_broker.X2GoBroker.get_agent_query_mode(): broker frontend overrides configured agent query mode ("{mode}"), using mode agent query mode: "{new_mode}".'.format(mode=_mode, new_mode=_new_mode)) return _new_mode else: return _mode
[docs] def get_agent_hostkey_policy(self, profile_id): """\ Get the agent hostkey policy (either of 'RejectPolicy', 'AutoAddPolicy' or 'WarningPolicy') that is configured for this X2Go Session Broker instance. The returned policy names match the MissingHostkeyPolicy class names as found in Python Paramiko. :returns: agent hostkey policy :rtype: ``str`` """ _default_agent_hostkey_policy = "RejectPolicy" _backend_agent_hostkey_policy = "" _agent_hostkey_policy = "" _profile = self.get_profile_broker(profile_id) if _profile and 'broker-agent-hostkey-policy' in _profile and _profile['broker-agent-hostkey-policy']: _agent_hostkey_policy = _profile['broker-agent-hostkey-policy'] logger_broker.debug('base_broker.X2GoBroker.get_agent_hostkey_policy(): found broker-agent-hostkey-policy in session profile with ID {id}: {value}. This one has precendence over the default and the backend value.'.format(id=profile_id, value=_agent_hostkey_policy)) elif self.config.has_value('broker_{backend}'.format(backend=self.backend_name), 'agent-hostkey-policy') and self.config.get_value('broker_{backend}'.format(backend=self.backend_name), 'agent-hostkey-policy'): _backend_agent_hostkey_policy = self.config.get_value('broker_{backend}'.format(backend=self.backend_name), 'agent-hostkey-policy') logger_broker.debug('base_broker.X2GoBroker.get_agent_hostkey_policy(): found agent-hostkey-policy in backend config section »{backend}«: {value}. This one has precendence over the default value.'.format(backend=self.backend_name, value=_agent_hostkey_policy)) elif self.config.has_value('global', 'default-agent-hostkey-policy') and self.config.get_value('global', 'default-agent-hostkey-policy'): _default_agent_hostkey_policy = self.config.get_value('global', 'default-agent-hostkey-policy') logger_broker.debug('base_broker.X2GoBroker.get_agent_hostkey_policy(): found default-agent-hostkey-policy in global config section: {value}'.format(value=_default_agent_hostkey_policy)) _policy = _agent_hostkey_policy or _backend_agent_hostkey_policy or _default_agent_hostkey_policy if _policy not in ('AutoAddPolicy', 'RejectPolicy', 'WarningPolicy'): logger_broker.warn('base_broker.X2GoBroker.get_agent_hostkey_policy(): given hostkey policy ({policy}) is invalid/unknown, falling back to default hostkey policy ({default_policy}).'.format(policy=_policy, default_policy=_default_agent_hostkey_policy)) _policy = _default_agent_hostkey_policy return _policy
[docs] def get_session_autologin(self, profile_id): """\ Detect if the given profile is configured to try automatic session logons. :returns: ``True`` to denote that automatic session login should be attempted :rtype: ``bool`` """ _default_session_autologin = False _session_autologin = False _profile = self.get_profile_broker(profile_id) if _profile and 'broker-session-autologin' in _profile and _profile['broker-session-autologin']: _session_autologin = _profile['broker-session-autologin'] if type(_session_autologin) == str: _session_autologin = _session_autologin.lower() in ('1', 'true') logger_broker.debug('base_broker.X2GoBroker.get_session_autologin(): found broker-session-autologin in session profile with ID {id}: {value}. This one has precendence over the default value.'.format(id=profile_id, value=_session_autologin)) elif self.config.has_value('global', 'default-session-autologin'): _default_session_autologin = self.config.get_value('global', 'default-session-autologin') logger_broker.debug('base_broker.X2GoBroker.get_session_autologin(): found default-session-autologin in global config section: {value}'.format(value=_default_session_autologin)) return _session_autologin or _default_session_autologin
# API compat name: use_session_autologin = get_session_autologin
[docs] def get_portscan_x2goservers(self, profile_id): """\ Detect if the given profile is configured to try portscanning on X2Go Servers before offering an X2Go Server hostname to the client. :returns: ``True`` if X2Go Servers shall be probed before offering it to clients :rtype: ``bool`` """ _default_portscan_x2goservers = False _portscan_x2goservers = False _profile = self.get_profile_broker(profile_id) if _profile and 'broker-portscan-x2goservers' in _profile and _profile['broker-portscan-x2goservers']: _portscan_x2goservers = _profile['broker-portscan-x2goservers'] if type(_portscan_x2goservers) == str: _portscan_x2goservers = _portscan_x2goservers.lower() in ('1', 'true') logger_broker.debug('base_broker.X2GoBroker.get_portscan_x2goservers(): found broker-portscan-x2goservers in session profile with ID {id}: {value}. This one has precendence over the default value.'.format(id=profile_id, value=_portscan_x2goservers)) elif self.config.has_value('global', 'default-portscan-x2goservers'): _default_portscan_x2goservers = self.config.get_value('global', 'default-portscan-x2goservers') logger_broker.debug('base_broker.X2GoBroker.get_portscan_x2goservers(): found default-portscan-x2goservers in global config section: {value}'.format(value=_default_portscan_x2goservers)) return _portscan_x2goservers or _default_portscan_x2goservers
# API compat name: use_portscan_x2goservers = get_portscan_x2goservers
[docs] def get_authorized_keys_file(self, profile_id): """\ Get the default location of server-side authorized_keys files used with the X2Go Session Broker. The file location can be configured broker-wide. It is also possible to provide a broker-authorized-keys file in session profiles. The latter will override the broker-wide conigured file location. :returns: authorized_keys location on the remote server :rtype: ``str`` """ _default_authorized_keys_file = "%h/.x2go/authorized_keys" _authorized_keys_file = "" _profile = self.get_profile_broker(profile_id) if _profile and 'broker-authorized-keys' in _profile and _profile['broker-authorized-keys']: _authorized_keys_file = _profile['broker-authorized-keys'] logger_broker.debug('base_broker.X2GoBroker.get_authorized_keys_file(): found broker-authorized-keys in session profile with ID {id}: {value}. This one has precendence over the default value.'.format(id=profile_id, value=_authorized_keys_file)) elif self.config.has_value('global', 'default-authorized-keys'): _default_authorized_keys_file = self.config.get_value('global', 'default-authorized-keys') logger_broker.debug('base_broker.X2GoBroker.get_authorized_keys_file(): found default-authorized-keys in global config section: {value}'.format(value=_default_authorized_keys_file)) return _authorized_keys_file or _default_authorized_keys_file
[docs] def get_sshproxy_authorized_keys_file(self, profile_id): """\ Get the default location of SSH proxy server-side authorized_keys files used with the X2Go Session Broker. The file location can be configured broker-wide. It is also possible to provide a broker-authorized-keys file in session profiles. The latter will override the broker-wide conigured file location. :returns: authorized_keys location on the remote SSH proxy server :rtype: ``str`` """ _default_authorized_keys_file = "%h/.x2go/authorized_keys" _authorized_keys_file = "" _profile = self.get_profile_broker(profile_id) if _profile and 'broker-sshproxy-authorized-keys' in _profile and _profile['broker-sshproxy-authorized-keys']: _authorized_keys_file = _profile['broker-sshproxy-authorized-keys'] logger_broker.debug('base_broker.X2GoBroker.get_sshproxy_authorized_keys_file(): found broker-sshproxy-authorized-keys in session profile with ID {id}: {value}. This one has precendence over the default value.'.format(id=profile_id, value=_authorized_keys_file)) elif self.config.has_value('global', 'default-sshproxy-authorized-keys'): _default_authorized_keys_file = self.config.get_value('global', 'default-sshproxy-authorized-keys') logger_broker.debug('base_broker.X2GoBroker.get_sshproxy_authorized_keys_file(): found default-sshproxy-authorized-keys in global config section: {value}'.format(value=_default_authorized_keys_file)) return _authorized_keys_file or _default_authorized_keys_file
[docs] def get_userdb_service(self): """\ Get the name of the backend being used for retrieving user information from the system. :returns: user service name :rtype: ``str`` """ _user_db = "libnss" if self.config.has_value('global', 'default-user-db'): _user_db = self.config.get_value('global', 'default-user-db').lower() or _user_db if self.config.has_value('broker_{backend}'.format(backend=self.backend_name), 'user-db'): _user_db = self.config.get_value('broker_{backend}'.format(backend=self.backend_name), 'user-db').lower() or _user_db return _user_db
[docs] def get_groupdb_service(self): """\ Get the name of the backend being used for retrieving group information from the system. :returns: group service name :rtype: ``str`` """ _group_db = "libnss" if self.config.has_value('global', 'default-group-db'): _group_db = self.config.get_value('global', 'default-group-db').lower() or _group_db if self.config.has_value('broker_{backend}'.format(backend=self.backend_name), 'group-db'): _group_db = self.config.get_value('broker_{backend}'.format(backend=self.backend_name), 'group-db').lower() or _group_db return _group_db
[docs] def get_use_load_checker(self): """\ Is this broker backend configured to access an X2Go Broker LoadChecker daemon. :returns: ``True`` if there should a load checker daemon running. :rtype: ``bool`` """ _use_load_checker = False if self.config.has_value('global', 'default-use-load-checker'): _use_load_checker = self.config.get_value('global', 'default-use-load-checker') or _use_load_checker if self.config.has_value('broker_{backend}'.format(backend=self.backend_name), 'use-load-checker'): _use_load_checker = self.config.get_value('broker_{backend}'.format(backend=self.backend_name), 'use-load-checker') or _use_load_checker return _use_load_checker
[docs] def use_load_checker(self, profile_id): """\ Actually query the load checker daemon for the given session profile ID. This method will check: - broker backend configured per backend or globally to use load checker daemon? - or on a per session profile basis? - plus: more than one host configured for the given session profile? :param profile_id: choose remote agent for this profile ID :type profile_id: ``str`` :returns: ``True`` if there is a load checker daemon running. :rtype: ``bool`` """ _profile_broker = self.get_profile_broker(profile_id) if not _profile_broker: return False _profile_session = self.get_profile(profile_id) # only use load checker if... # more than one host is defined in the session profile if len(_profile_session['host']) < 2: return False # if not blocked on a per session profile basis if 'broker-use-load-checker' in _profile_broker and _profile_broker['broker-use-load-checker'] not in ('1', 'true', 'TRUE', 'True'): return False # if load checking is enabled globally, for the broker backend, # or for the given session profile... if self.get_use_load_checker() or ('broker-use-load-checker' in _profile_broker and _profile_broker['broker-use-load-checker'] in ('1', 'true', 'TRUE', 'True')): return True return False
def _import_nameservice_module(self, service='libnss'): try: if self.nameservice_module is None: _nameservice_module = None namespace = {} exec("import x2gobroker.nameservices.{service}_nameservice as _nameservice_module".format(service=service), namespace) self.nameservice_module = namespace['_nameservice_module'] return True except ImportError: return False
[docs] def has_user(self, username): """\ Test if the broker knows user ``<username>``. :param username: test for existence of this user :type username: ``str`` :returns: returns ``True`` if a user exists :rtype: ``bool`` """ if self._import_nameservice_module(service=self.get_userdb_service()): return self.nameservice_module.X2GoBrokerNameService().has_user(username=username) else: return False
[docs] def get_users(self): """\ Get list of known users. :returns: returns list of known users :rtype: ``list`` """ if self._import_nameservice_module(service=self.get_userdb_service()): return self.nameservice_module.X2GoBrokerNameService().get_users() else: return False
[docs] def has_group(self, group): """\ Test if the broker knows group ``<group>``. :param group: test for existence of this group :type group: ``str`` :returns: returns ``True`` if a group exists :rtype: ``bool`` """ if self._import_nameservice_module(service=self.get_groupdb_service()): return self.nameservice_module.X2GoBrokerNameService().has_group(group=group) else: return False
[docs] def get_groups(self): """\ Get list of known groups. :returns: returns list of known groups :rtype: ``list`` """ if self._import_nameservice_module(service=self.get_groupdb_service()): return self.nameservice_module.X2GoBrokerNameService().get_groups() else: return False
[docs] def get_primary_group(self, username): """\ Get the primary group of a given user. :param username: get primary group for this username :type username: ``str`` :returns: returns the name of the primary group :rtype: ``str`` """ if self._import_nameservice_module(service=self.get_groupdb_service()): return self.nameservice_module.X2GoBrokerNameService().get_primary_group(username) else: return False
[docs] def is_group_member(self, username, group, primary_groups=False): """\ Check if a user is member of a given group. :param username: check group membership of this user :type username: ``str`` :param group: test if user is member of this group :type group: ``str`` :param primary_groups: if ``True``, test for primary group membership, as well :type primary_groups: ``bool`` :returns: returns ``True`` if the user is member of the given group :rtype: ``bool`` """ if self._import_nameservice_module(service=self.get_groupdb_service()): return self.nameservice_module.X2GoBrokerNameService().is_group_member(username=username, group=group, primary_groups=primary_groups) else: return []
[docs] def get_group_members(self, group, primary_groups=False): """\ Get the list of members in group ``<group>``. :param group: valid group name :type group: ``str`` :param primary_groups: include primary groups found with the user db service :type primary_groups: ``bool`` :returns: list of users belonging to the given group :rtype: ``list`` """ if self._import_nameservice_module(service=self.get_groupdb_service()): return self.nameservice_module.X2GoBrokerNameService().get_group_members(group=group, primary_groups=primary_groups) else: return []
[docs] def get_user_groups(self, username, primary_groups=False): """\ Get all groups a given user is member of. :param username: get groups for this user :type username: ``str`` :param primary_groups: if ``True``, include the user's primary group in the group list :type primary_groups: ``bool`` :returns: list of groups the given user is member of :rtype: ``list`` """ if self._import_nameservice_module(service=self.get_groupdb_service()): return self.nameservice_module.X2GoBrokerNameService().get_user_groups(username=username, primary_groups=primary_groups) else: return []
[docs] def check_access(self, username='', password='', ip='', cookie=None, override_password_auth=False): """\ Check if a given user with a given password may gain access to the X2Go session broker. :param username: a username known to the session broker :type username: ``str`` :param password: a password that authenticates the user against the X2Go session broker :type password: ``str`` :param ip: the ip address of the client :type ip: ``str`` :param cookie: an extra (static or dynamic) authentication token :type cookie: ``str`` :param override_password_auth: let password auth always succeed, needed for SSH broker (where SSH handled the password (or key) based authentication :type override_password_auth: ``bool`` :returns: returns ``True`` if the authentication has been successful :rtype: ``bool``,``str`` """ require_password = self.config.get_value('global', 'require-password') require_cookie = self.config.get_value('global', 'require-cookie') # LEGACY support for X2Go Session Broker (<< 0.0.3.0) configuration files if not self.config.get_value('global', 'check-credentials'): logger_broker.warning('base_broker.X2GoBroker.check_access(): deprecated parameter \'check-credentials\' used in x2gobroker.conf (use \'require-password\' and \'require-cookie\' instead)!!!'.format(configfile=self.config_file)) require_password = False require_cookie = False ### FOR INTRANET LOAD BALANCER WE MAY JUST ALLOW ACCESS TO EVERYONE ### This is handled through the config file, normally /etc/x2go/x2gobroker.conf if not require_password and not require_cookie: logger_broker.debug('base_broker.X2GoBroker.check_access(): access is granted without checking credentials, prevent this in {configfile}'.format(configfile=self.config_file)) return True, None elif username == 'check-credentials' and password == 'FALSE': # this catches a validation check from the UCCS web frontend... return False, None ### IMPLEMENT YOUR AUTHENTICATION LOGIC IN THE self._do_authenticate(**kwargs) METHOD ### when inheriting from the x2gobroker.brokers.base_broker.X2GoBroker class. if type(cookie) is bytes: cookie = cookie if (((cookie == None) or (cookie == "")) and require_cookie): #cookie required but we did not get one - catch wrong cookie case later logger_broker.debug('base_broker.X2GoBroker.check_access(): cookie required but none given.') return False, None # check if cookie sent was our preset cookie from config file next_cookie = self.get_my_cookie() access = (cookie == next_cookie ) logger_broker.debug('base_broker.X2GoBroker.check_access(): checking if our configured cookie was submitted: {access}'.format(access=access)) # the require cookie but not password case falls through to returning value of access if require_password: # using files to store persistant cookie information because global variables do not work across threads in WSGI if _X2GOBROKER_USER == _X2GOBROKER_DAEMON_USER: cookie_directory = self.config.get_value('global', 'cookie-directory') cookie_directory = os.path.normpath(cookie_directory) else: cookie_directory=os.path.normpath(os.path.expanduser('~/.x2go/broker-cookies/')) if (not os.path.isdir(cookie_directory)): logger_broker.debug('base_broker.X2GoBroker.check_access(): cookie-directory {cookie_directory} does not exist trying to create it'.format(cookie_directory=cookie_directory)) try: os.makedirs(cookie_directory); except: logger_broker.warning('base_broker.X2GoBroker.check_access(): could not create cookie-directory {cookie_directory} failing to authenticate'.format(cookie_directory=cookie_directory)) return False, None if access or cookie == None or cookie == "": # this should be the first time we have seen this user or they are using old client so verify their passwrd ### IMPLEMENT YOUR AUTHENTICATION LOGIC IN THE self._do_authenticate(**kwargs) METHOD ### when inheriting from the x2gobroker.brokers.base_brokers.X2GoBroker class. access = self._do_authenticate(username=username, password=password) or override_password_auth ### ### if access: logger_broker.warning('base_broker.X2GoBroker.check_access(): authentication succeeded for user {username} at ip {ip}'.format(username=username, ip=ip)) #create new cookie for this user #each user gets one or more tuples of IP, time stored as username_UUID files so they can connect from multiple sessions next_cookie = str(uuid.uuid4()) if cookie_directory and username and next_cookie: fh = open(cookie_directory+"/"+username+"_"+next_cookie,"w") fh.write('{ip} {time}'.format(ip=ip, time=time.time())) fh.close() if cookie_directory and username and cookie: os.remove(cookie_directory+"/"+username+"_"+cookie) logger_broker.debug('base_broker.X2GoBroker.check_access(): Giving new cookie: {cookie} to user {username} at ip {ip}'.format(cookie=next_cookie,username=username,ip=ip)) else: logger_broker.warning('base_broker.X2GoBroker.check_access(): authentication failed for user {username} at ip {ip}'.format(username=username, ip=ip)) else: # there is a cookie but its not ours so its either wrong or subsequent password auth if os.path.isfile(cookie_directory+"/"+username+"_"+cookie): logger_broker.debug('base_broker.X2GoBroker.check_access(): found valid auth key for user cookie: {usercookie}'.format(usercookie=username+"_"+cookie)) fh=open(cookie_directory+"/"+username+"_"+cookie,"r") origip,origtime= fh.read().split() fh.close() os.unlink(cookie_directory+"/"+username+"_"+cookie) # found cookie - make sure IP and time are good if self.config.get_value('global', 'verify-ip') and (ip != origip): logger_broker.debug('base_broker.X2GoBroker.check_access(): IPs differ (new: {ip} old: {origip}) - rejecting user'.format(ip=ip,origip=origip)) return False, None if (time.time() - float(origtime)) > self.config.get_value('global', 'auth-timeout'): logger_broker.debug('base_broker.X2GoBroker.check_access(): Too much time elapsed since origional auth - rejecting user') return False, None if self.config.get_value('global', 'use-static-cookie'): #if using static cookies keep same cookie as user presented next_cookie = cookie else: #otherwise give them new random cookie next_cookie = str(uuid.uuid4()) logger_broker.debug('base_broker.X2GoBroker.check_access(): Giving cookie: {cookie} to ip {ip}'.format(cookie=next_cookie, ip=ip)) fh = open(cookie_directory+"/"+username+"_"+next_cookie,"w") fh.write('{ip} {time}'.format(ip=ip, time=origtime)) fh.close() access = True else: # FIXME: here we need some magic to remove deprecated cookie files (by their timestamp)!!! # client sent us an unknown cookie so failing auth logger_broker.debug('base_broker.X2GoBroker.check_access(): User {username} from {ip} presented cookie {cookie} which is not recognized - rejecting user'.format(username=username, cookie=cookie, ip=ip)) return False, None return access, next_cookie
[docs] def get_remote_agent(self, profile_id, exclude_agents=[], ): """\ Randomly choose a remote agent for agent query. :param profile_id: choose remote agent for this profile ID :type profile_id: ``str`` :param exclude_agents: a list of remote agent dict objects to be exclude from the random choice :type exclude_agents: ``list`` :returns: remote agent to use for queries for profile ID :rtype: ``dict`` """ remote_agent = None # no remote agent needed for shadow sessions if self.is_shadow_profile(profile_id): return remote_agent agent_query_mode = self.get_agent_query_mode(profile_id).upper() if agent_query_mode == 'SSH' and x2gobroker.agent.has_remote_broker_agent_setup(): profile = self.get_profile(profile_id) server_list = profile['host'] random.shuffle(server_list) # if the load checker is in use for this profile, let's retrieve the available server loads here # because: # - it is fast... # - if hosts are marked as "HOST-UNREACHABLE", we don't have to attempt # using them as a remote agent (reduce delays at session # startup/resumption) # - the retrieved load factors can be re-used in X2GoBroker.select_session(). load_factors = {} if self.use_load_checker(profile_id): load_factors = x2gobroker.loadchecker.check_load(self.backend_name, profile_id) for h in [ _h for _h in list(load_factors.keys()) if type(load_factors[_h]) != int ]: if h in server_list: server_list.remove(h) for agent in exclude_agents: if agent['hostname'] in server_list: server_list.remove(agent['hostname']) while server_list: remote_agent_hostname = server_list[-1] remote_agent_hostaddr = remote_agent_hostname remote_agent_port = profile['sshport'] if 'sshport={hostname}'.format(hostname=remote_agent_hostname) in profile: remote_agent_port = profile["sshport={hostname}".format(hostname=remote_agent_hostname)] if 'host={hostname}'.format(hostname=remote_agent_hostname) in profile: remote_agent_hostaddr = profile["host={hostname}".format(hostname=remote_agent_hostname)] remote_agent = { 'hostname': remote_agent_hostname, 'hostaddr': remote_agent_hostaddr, 'port': remote_agent_port, 'host_key_policy': self.get_agent_hostkey_policy(profile_id), } try: if x2gobroker.agent.ping(remote_agent=remote_agent): break except x2gobroker.x2gobroker_exceptions.X2GoBrokerAgentException: # at the end of this loop, an empty dict means: no X2Go Server could be contacted!!! remote_agent = {} server_list = server_list[0:-1] if not remote_agent: logger_broker.warning('base_broker.X2GoBroker.get_remote_agent(): failed to allocate any broker agent (query-mode: {query_mode}, remote_agent: {remote_agent})'.format(query_mode=agent_query_mode, remote_agent=remote_agent)) else: # ship the load_factors retrieved from the load checker service in the remote_agent dict remote_agent['load_factors'] = load_factors elif agent_query_mode == 'LOCAL': # use a non-False value here, not used anywhere else... remote_agent = 'LOCAL' return remote_agent
[docs] def get_all_remote_agents(self, profile_id): """\ Get all remote agents. :param profile_id: choose remote agent for this profile ID :type profile_id: ``str`` :returns: ``list`` of remote agents for the given profile ID :rtype: ``list`` """ remote_agents = [] # no remote agent needed for shadow sessions if self.is_shadow_profile(profile_id): return remote_agents agent_query_mode = self.get_agent_query_mode(profile_id).upper() if agent_query_mode == 'SSH' and x2gobroker.agent.has_remote_broker_agent_setup(): profile = self.get_profile(profile_id) server_list = profile['host'] while server_list: remote_agent_hostname = server_list[-1] remote_agent_hostaddr = remote_agent_hostname remote_agent_port = profile['sshport'] if 'sshport={hostname}'.format(hostname=remote_agent_hostname) in profile: remote_agent_port = profile["sshport={hostname}".format(hostname=remote_agent_hostname)] if 'host={hostname}'.format(hostname=remote_agent_hostname) in profile: remote_agent_hostaddr = profile["host={hostname}".format(hostname=remote_agent_hostname)] remote_agents.append({ 'hostname': remote_agent_hostname, 'hostaddr': remote_agent_hostaddr, 'port': remote_agent_port, } ) server_list = server_list[0:-1] return remote_agents
[docs] def is_shadow_profile(self, profile_id): """\ Detect from the session profile, if it defines a desktop sharing (shadow) session. :param profile_id: ID of a valid session profile :type profile_id: ``str`` :returns: ``True`` if the session profile defines a desktop sharing (shadow) session :rtype: ``bool`` """ profile = self.get_profile(profile_id) return profile['command'] == "SHADOW"
[docs] def check_for_sessions(self, profile_id): """\ Detect from the session profile, if we should query the remote broker agent for running or suspended sessions. :param profile_id: ID of a valid session profile :type profile_id: ``str`` :returns: ``True`` if the remote broker agent should be queried for running/suspended sessions :rtype: ``bool`` """ do_check = True # do check, for all commands except the "SHADOW" command do_check = do_check and not self.is_shadow_profile(profile_id) return do_check
[docs] def get_profile_for_user(self, profile_id, username, broker_frontend=None): """\ Expect a profile id and perform some checks and preparations to make it ready for exporting to a broker client: - drop internal host=<hostname> and sshport=<port> keys from the profile, broker clients cannot handle those - drop keys with value "not-set" - replace BROKER_USER by the name of the authenticated user - test if autologin is possible - fix rootless session profile option for non-desktop sessions - perform an ACL check (return ``None`` if it fails) - query a remote agent (if configured) to check if we have running / suspended sessions on the remote X2Go Server :param profile_id: ID of a valid session profile :type profile_id: ``str`` :param username: prepare session profile for this (authenticated) user :type username: ``str`` :param broker_frontend: some broker frontend (e.g. UCCS) require special treatment by this method :type broker_frontend: ``str`` :returns: session profile as a dictionary (ready for sending out to a broker client) :rtype: ``dict`` """ profile = self.get_profile(profile_id) acls = self.get_profile_acls(profile_id) if self.check_profile_acls(username, acls): for key in list(copy.deepcopy(profile).keys()): if profile[key] == "not-set": del profile[key] continue if key.startswith('host=') and broker_frontend != 'uccs': del profile[key] if key.startswith('sshport=') and broker_frontend != 'uccs': del profile[key] if key == 'user' and profile[key] == 'BROKER_USER': profile[key] = username if self.get_session_autologin(profile_id): profile['autologin'] = True profile['key'] = '<will-be-exchanged-during-session-selection>' # make sure that desktop sessions (that we know by name) do run with rootless=false # and that the command string is always upper case (otherwise x2goruncommand might # stumble over it...) if profile['command'].upper() in x2gobroker.defaults.X2GO_DESKTOP_SESSIONS: profile['rootless'] = False profile['command'] = profile['command'].upper() remote_agent = self.get_remote_agent(profile_id) if self.check_for_sessions(profile_id): if remote_agent: try: success, running_sessions, suspended_sessions = x2gobroker.agent.has_sessions(username, remote_agent=remote_agent) if running_sessions: logger_broker.debug('base_broker.X2GoBroker.get_profile_for_user(): found running sessions on host(s): {hosts}'.format(hosts=', '.join(running_sessions))) if suspended_sessions: logger_broker.debug('base_broker.X2GoBroker.get_profile_for_user(): found running sessions on host(s): {hosts}'.format(hosts=', '.join(suspended_sessions))) suspended_matching_hostnames = x2gobroker.utils.matching_hostnames(profile['host'], suspended_sessions) running_matching_hostnames = x2gobroker.utils.matching_hostnames(profile['host'], running_sessions) if suspended_matching_hostnames: profile['status'] = 'S' profile['host'] = [suspended_matching_hostnames[0]] elif running_matching_hostnames: profile['status'] = 'R' profile['host'] = [running_matching_hostnames[0]] else: profile['host'] = [profile['host'][0]] if 'status' in profile and profile['status']: logger_broker.debug('base_broker.X2GoBroker.get_profile_for_user(): marking session profile {name} as {status}'.format(name=profile['name'], status=profile['status'])) except x2gobroker.x2gobroker_exceptions.X2GoBrokerAgentException as e: logger_broker.warning('base_broker.X2GoBroker.get_profile_for_user(): broker agent call failed. Error message is: {errmsg}'.format(errmsg=str(e))) else: profile['host'] = [profile['host'][0]] return profile else: return None
[docs] def list_profiles(self, username): """\ Retrieve a list of available session profiles for the authenticated user. :param username: query session profile list for this user :type username: ``str`` :returns: list of profile dictionaries :rtype: ``dict`` """ list_of_profiles = {} for profile_id in self.get_profile_ids_for_user(username): profile = self.get_profile_for_user(profile_id, username) if profile: list_of_profiles.update({profile_id: profile, }) return list_of_profiles
[docs] def select_session(self, profile_id, username=None, pubkey=None): """\ Start/resume a session by selecting a profile name offered by the X2Go client. The X2Go server that the session is launched on is selected automatically by the X2Go session broker. :param profile_id: the selected profile ID. This matches one of the dictionary keys offered by the ``list_profiles`` method :type profile_id: ``str`` :param username: specify X2Go Server username that this operation runs for :type username: ``str`` :param pubkey: The broker clients may send us a public key that we may temporarily install into a remote X2Go Server for non-interactive login :type pubkey: ``str`` :returns: the seclected session (X2Go session ID) :rtype: ``str`` """ try: profile = self.get_profile(profile_id) except x2gobroker.x2gobroker_exceptions.X2GoBrokerProfileException: return { 'server': 'no-server-available', 'port': 22, } # if we have more than one server, pick one server randomly for X2Go Broker Agent queries server_list = profile['host'] if len(server_list) == 0: return { 'server': 'no-server-available', 'port': profile['sshport'], } # if everything below fails, this will be the X2Go Server's hostname that # we will connect to... server_name = server_list[0] server_port = profile['sshport'] # try to retrieve a remote broker agent remote_agent = self.get_remote_agent(profile_id) # check for already running sessions for the given user (if any is given) session_list = [] if remote_agent and username: try: success, session_list = x2gobroker.agent.list_sessions(username=username, remote_agent=remote_agent) except x2gobroker.x2gobroker_exceptions.X2GoBrokerAgentException: session_list = [] session_info = None selected_session = {} busy_servers = None _save_server_list = None _save_busy_servers = None initial_server_list = copy.deepcopy(server_list) agent_query_mode_is_SSH = self.get_agent_query_mode(profile_id).upper() == 'SSH' while not selected_session and server_list: # X2Go Server uses the system's hostname, so let's replace # that here automatically, if we tested things via localhost... for h in server_list: if h == 'localhost': server_list.remove(h) server_list.append(socket.gethostname()) matching_server_names = None if session_list: matching_server_names = x2gobroker.utils.matching_hostnames(server_list, [ si.split('|')[3] for si in session_list ]) if remote_agent == {}: # we failed to contact any remote agent, so it is very likely, that all servers are down... server_list = [] elif session_list and matching_server_names: # Obviously a remote broker agent reported an already running session # on the / on one the available X2Go Server host(s) # When resuming, always select the first session in the list, # there should only be one running/suspended session by design # of X2Go brokerage (this may change in the future) try: running_sessions = [] suspended_sessions = [] for session_info in session_list: if session_info.split('|')[3] in matching_server_names: if session_info.split('|')[4] == 'R': running_sessions.append(session_info) if session_info.split('|')[4] == 'S': suspended_sessions.append(session_info) if suspended_sessions or running_sessions: # we prefer suspended sessions over resuming sessions if we find sessions with both # states of activity if suspended_sessions: session_info = suspended_sessions[0] elif running_sessions: session_info = running_sessions[0] x2gobroker.agent.suspend_session(username=username, session_name=session_info.split('|')[1], remote_agent=remote_agent) # this is the turn-around in x2gocleansessions, so waiting as along as the daemon # that will suspend the session time.sleep(2) session_info = session_info.replace('|R|', '|S|') # only use the server's official hostname (as set on the server) # if we have been provided with a physical server address. # If no physical server address has been provided, we have to use # the host address as found in server_list (and hope we can connect # to that address. _session_server_name = session_info.split('|')[3] if 'host={server_name}'.format(server_name=_session_server_name) in profile: server_name = _session_server_name elif _session_server_name in server_list: server_name = _session_server_name elif x2gobroker.utils.matching_hostnames(server_list, [_session_server_name]): for _server_name in server_list: if _server_name.startswith(_session_server_name): server_name = _server_name break else: logger_broker.error('base_broker.X2GoBroker.select_session(): configuration error. Hostnames in session profile and actual server names do not match, we won\'t be able to resume/take-over a session this time') # choosing a random server from the server list, to end up anywhere at least... server_name = random.choice(server_list) except IndexError: # FIXME: if we get here, we have to deal with a broken session info # entry in the X2Go session database. -> AWFUL!!! pass # detect best X2Go server for this user if load balancing is configured elif remote_agent and len(server_list) >= 2 and username: # No running / suspended session was found on any of the available # X2Go Servers. Thus, we will try to detect the best server for this # load balanced X2Go Server farm. # query remote agent on how busy our servers are... (if a selected server is down # and we come through here again, don't query business state again, use the remembered # status) if busy_servers is None: try: if agent_query_mode_is_SSH and remote_agent['load_factors']: success, busy_servers = x2gobroker.agent.get_servers(username=username, remote_agent=remote_agent) else: success, busy_servers = x2gobroker.agent.find_busy_servers(username=username, remote_agent=remote_agent) except x2gobroker.x2gobroker_exceptions.X2GoBrokerAgentException: pass if busy_servers is not None: # if we do not get here, we failed to query a valid agent... # when detecting the server load we have to support handling of differing subdomains (config # file vs. server load returned by x2gobroker agent). Best approach: all members of a multi-node # server farm either # # (a) do not have a subdomain in their hostname or # (b) have an identical subdomain in their hostnames # Example: # # ts01, ts02 - hostnames as returned by agent # ts01.intern, ts02.intern - hostnames configured in session profile option ,,host'' # -> this will result in the subdomain .intern being stripped off from the hostnames before # detecting the best server for this user ### NORMALIZE (=reduce to hostname only) X2Go server names (as found in config) if possible server_list_normalized, subdomains_config = x2gobroker.utils.normalize_hostnames(server_list) ### NORMALIZE X2Go server names (as returned by broker agent)--only if the hostnames in # the config share the same subdomain if len(subdomains_config) == 1: busy_servers_normalized, subdomains_agent = x2gobroker.utils.normalize_hostnames(busy_servers) if len(subdomains_agent) <= 1: # all X2Go servers in the multi-node server farm are in the same DNS subdomain # we can operate on hostname-only hostnames _save_server_list = copy.deepcopy(server_list) _save_busy_servers = copy.deepcopy(busy_servers) server_list = server_list_normalized busy_servers = busy_servers_normalized # the list of busy_servers only shows servers with sessions, but not those servers that are entirely idle... for server in server_list: if server not in list(busy_servers.keys()): busy_servers[server] = 0 # we will only contact servers that are (still) in server_list for busy_server in list(busy_servers.keys()): if busy_server not in server_list: del busy_servers[busy_server] # dynamic load-balancing via load checker service if agent_query_mode_is_SSH and remote_agent['load_factors']: load_factors = remote_agent['load_factors'] busy_servers_temp = copy.deepcopy(busy_servers) for busy_server in list(busy_servers_temp.keys()): if busy_server in list(load_factors.keys()) and type(load_factors[busy_server]) is not int: # if a host cannot report its load, let's ignore it... del busy_servers_temp[busy_server] elif busy_server in list(load_factors.keys()) and ( type(load_factors[busy_server]) is int or busy_servers[busy_server] == 0): # when using the load checker service, then busy_servers contains the number of sessions per host # do the load-factor / numSessions calculation here... (avoid divison-by-zero by adding +1 to # the number of sessions here) busy_servers_temp[busy_server] = 1.0 / (load_factors[busy_server] / ( busy_servers[busy_server] +1)) else: # ignore the load checker, results are garbage... busy_servers_temp = None break if busy_servers_temp is not None: busy_servers = copy.deepcopy(busy_servers_temp) busy_server_list = [ (load, server) for server, load in list(busy_servers.items()) ] busy_server_list.sort() logger_broker.debug('base_broker.X2GoBroker.select_session(): load balancer analysis: {server_load}'.format(server_load=busy_server_list)) server_name = busy_server_list[0][1] # this makes sure we allow back-translation of hostname to host address # when the format "<hostname> (<ip-address>)" ist used in the hosts field... if len(subdomains_config) == 1: server_name += '.{domain}'.format(domain=subdomains_config[0]) if _save_server_list: server_list = copy.deepcopy(_save_server_list) _save_server_list = None if _save_busy_servers: busy_servers = copy.deepcopy(_save_busy_servers) _save_busy_servers = None else: logger_broker.warning('base_broker.X2GoBroker.select_session(): no broker agent could be contacted, this does not look good. We tried these agent hosts: {agent_hosts}'.format(agent_hosts=initial_server_list)) # detect best X2Go server for this user if load balancing is configured elif len(server_list) >= 2: if self.is_shadow_profile(profile_id): # we will ignore load-balancing for desktop sharing profiles server_list = [server_list[0]] server_name = server_list[0] else: # no remote broker agent or no username? Let's play roulette then... server_name = random.choice(server_list) ### ### by now we should know the proper host to connect to... ### server_addr = server_name # if we have an explicit TCP/IP port server_name, let's use that instead... try: server_port = profile['sshport={hostname}'.format(hostname=server_name)] logger_broker.debug('base_broker.X2GoBroker.select_session(): use physical server port: {port}'.format(port=server_port)) except KeyError: pass # if we have an explicit TCP/IP address for server_name, let's use that instead... try: server_addr = profile['host={hostname}'.format(hostname=server_name)] logger_broker.debug('base_broker.X2GoBroker.select_session(): use physical server address: {address}'.format(address=server_addr)) except KeyError: pass if server_list: if not self.get_portscan_x2goservers(profile_id) or x2gobroker.utils.portscan(addr=server_name, port=server_port) or x2gobroker.utils.portscan(addr=server_addr, port=server_port): selected_session = { 'server': server_addr, 'port': server_port, } else: server_list.remove(server_name) # pick remaining server from server list (if any) if server_list: logger_broker.warning('base_broker.X2GoBroker.select_session(): failed to contact host \'{down_server}\', trying next server \'{next_server}\''.format(down_server=server_name, next_server=server_list[0])) server_name = server_list[0] else: logger_broker.error('base_broker.X2GoBroker.select_session(): no X2Go Server could be contacted, session startup will fail, tried these hosts: {server_list}'.format(server_list=initial_server_list)) # If we arrive here and session_list carries an entry for this user, then the session DB probably still # carries a zombie session entry (that will disappear when the down X2Go Server comes up again (cleanup # via x2gocleansessions). # # Thus, let's ignore this session and check if there is another appropriate session in session_list if session_info is not None: session_list.remove(session_info) session_info = None if not selected_session and not server_list: if len(initial_server_list) > 1: selected_session = { 'server': 'no-X2Go-Server-available', 'port': server_port, } else: # hand-over the original hostname for non-load-balanced session profiles failed_server_port = server_port failed_server_name = initial_server_list[0] try: failed_server_port = profile['port={hostname}'.format(hostname=failed_server_name)] except KeyError: pass try: failed_server_name = profile['host={hostname}'.format(hostname=failed_server_name)] except KeyError: pass selected_session = { 'server': failed_server_name, 'port': failed_server_port, } # are we resuming a running/suspended session? if session_info is not None: selected_session['session_info'] = session_info # define a remote SSH proxy agent if an SSH proxy host is used with this session profile if 'sshproxyhost' in profile and profile['sshproxyhost']: remote_sshproxy_agent = { 'hostname': profile['sshproxyhost'], 'hostaddr': profile['sshproxyhost'], 'port': "22" } if 'sshproxyport' in profile and profile['sshproxyport']: remote_sshproxy_agent['port'] = profile['sshproxyport'] else: remote_sshproxy_agent = None # session autologin feature if remote_agent and self.get_session_autologin(profile_id) and username: # let's use the chosen server_name if remote_agent is reachable via SSH if type(remote_agent) is dict: remote_agent = { 'hostname': server_name, 'hostaddr': server_addr, 'port': selected_session['port'], } if not pubkey: # if the broker client has not provided a public SSH key, we will generate one # this is the OLD style of the auto login feature # FIXME: we somehow have to find out about the username of the person at the broker client-side... # using the username used for server login for now... pubkey, privkey = x2gobroker.agent.genkeypair(local_username=username, client_address=self.get_client_address()) if remote_sshproxy_agent is not None: x2gobroker.agent.add_authorized_key(username=username, pubkey_hash=pubkey, authorized_keys_file=self.get_sshproxy_authorized_keys_file(profile_id), remote_agent=remote_sshproxy_agent, ), x2gobroker.agent.add_authorized_key(username=username, pubkey_hash=pubkey, authorized_keys_file=self.get_authorized_keys_file(profile_id), remote_agent=remote_agent, ), selected_session.update({ 'authentication_privkey': privkey, }) if remote_sshproxy_agent is not None: x2gobroker.agent.delete_authorized_key(username=username, pubkey_hash=pubkey, authorized_keys_file=self.get_sshproxy_authorized_keys_file(profile_id), remote_agent=remote_sshproxy_agent, delay_deletion=20, ) x2gobroker.agent.delete_authorized_key(username=username, pubkey_hash=pubkey, authorized_keys_file=self.get_authorized_keys_file(profile_id), remote_agent=remote_agent, delay_deletion=20, ) else: logger_broker.info('base_broker.X2GoBroker.select_session(): accepting public SSH key from broker client') if remote_sshproxy_agent is not None: x2gobroker.agent.add_authorized_key(username=username, pubkey_hash=pubkey, authorized_keys_file=self.get_sshproxy_authorized_keys_file(profile_id), remote_agent=remote_sshproxy_agent, ), x2gobroker.agent.add_authorized_key(username=username, pubkey_hash=pubkey, authorized_keys_file=self.get_authorized_keys_file(profile_id), remote_agent=remote_agent, ), selected_session.update({ 'authentication_pubkey': 'ACCEPTED', }) if remote_sshproxy_agent is not None: x2gobroker.agent.delete_authorized_key(username=username, pubkey_hash=pubkey, authorized_keys_file=self.get_sshproxy_authorized_keys_file(profile_id), remote_agent=remote_sshproxy_agent, delay_deletion=20, ) x2gobroker.agent.delete_authorized_key(username=username, pubkey_hash=pubkey, authorized_keys_file=self.get_authorized_keys_file(profile_id), remote_agent=remote_agent, delay_deletion=20, ) return selected_session
[docs] def change_password(self, new='', old=''): """\ Modify the authenticated user's password on the X2Go infrastructure (normally, one user in one X2Go site setup should have the same password on all machines). This function is a dummy function and needs to be overridden in specific broker backend implementations :param new: the new password that is to be set :type new: ``str`` :param old: the currently set password :type old: ``str`` :returns: whether the password change has been successful :rtype: ``bool`` """ return False
[docs] def run_optional_script(self, script_type, username, password, task, profile_id, ip, cookie, authed=None, server=None): """\ Run all optional scripts of type script_type. Called with 3 different script types: - pre_auth_scripts - before authentication happens - post_auth_scripts - after authentication but before anything else occurs - select_session_scripts - after load balancing before a specific server is sent to the client These scripts allow for both addional actions to be performed as well as the mangling of any relevant fields. :param script_type: name of the script type to be executed (``pre_auth_scripts``, ``post_auth_scripts``, ``select_session_scripts``) :type script_type: ``str`` :param username: name of the X2Go session user a script will run for :type username: ``str`` :param password: password for the X2Go session :type password: ``str`` :param task: the broker task that currently being processed :type task: ``str`` :param profile_id: the session profile ID that is being operated upon :type profile_id: ``str`` :param ip: the client machine's IP address :type ip: ``str`` :param cookie: the currently valid authentication cookie :type cookie: ``str`` :param authed: authentication status (already authenticated or not) :type authed: ``bool`` :param server: hostname or IP address of the X2Go server being operated upon :type server: ``str`` :returns: Pass-through of the return value returned by the to-be-run optional script (i.e., success or failure) :rtype: ``bool`` """ global_config = self.get_global_config() if len(global_config[script_type]) != 0: for script in global_config[script_type]: try: if script: my_script=None namespace = {} exec("import x2gobroker.optional_scripts.{script}_script\nmy_script = x2gobroker.optional_scripts.{script}_script.X2GoBrokerOptionalScript()".format(script=script), namespace) my_script = namespace['my_script'] logger_broker.debug ('Calling {script_type} {script} with username: {username}, password: {password}, task: {task}, profile_id: {profile_id}, ip: {ip}, cookie: {cookie}, authed: {authed}, server: {server}'.format(script_type=script_type,script=script,username=username, password='XXXXX', task=task, profile_id=profile_id, ip=ip, cookie=cookie, authed=authed, server=server)) username, password, task, profile_id, ip, cookie, authed, server = my_script.run_me(username=username, password=password, task=task, profile_id=profile_id, ip=ip, cookie=cookie, authed=authed, server=server) logger_broker.debug ('Finished {script_type} {script} with username: {username}, password: {password}, task: {task}, profile_id: {profile_id}, ip: {ip}, cookie: {cookie}, authed: {authed}, server: {server}'.format(script_type=script_type,script=script,username=username, password='XXXXX', task=task, profile_id=profile_id, ip=ip, cookie=cookie, authed=authed, server=server)) except ImportError: logger_error.error('No such optional script \'{script}\''.format(script=script)) return username, password, task, profile_id, ip, cookie, authed, server