# -*- 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_my_cookie(self):
"""\
Get the pre-set authentication cookie UUID hash that clients
have to use on their first connection attempt (if the global
config option "require-cookie" has been set).
:returns: the pre-set authentication cookie UUID hash
:rtype: ``str``
"""
unconfigured_my_cookie = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
my_cookie = unconfigured_my_cookie
deprecated_my_cookie = self.config.get_value('global', 'my-cookie')
my_cookie_file = self.config.get_value('global', 'my-cookie-file')
if os.path.isfile(my_cookie_file):
fh = open(my_cookie_file, "r")
lines = [ line for line in fh.read().split('\n') if line and not line.startswith('#') ]
my_cookie = lines[0].split(' ')[0]
if my_cookie != unconfigured_my_cookie:
return my_cookie
elif deprecated_my_cookie != unconfigured_my_cookie:
return deprecated_my_cookie
# instead of returning None here, we invent a cookie and return that
return str(uuid.uuid4())
[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