Source code for x2gobroker.brokers.base_broker

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

# Copyright (C) 2012-2019 by Mike Gabriel <mike.gabriel@das-netzwerkteam.de>
# Copyright (C) 2012-2019 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 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_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 ### ### logger_broker.debug('base_broker.X2GoBroker.check_access(): checking for valid authentication: {access}'.format(access=access)) if access: #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: # 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, } 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