#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Univention Management Console
# session handling
#
# Copyright 2006-2022 Univention GmbH
#
# https://www.univention.de/
#
# All rights reserved.
#
# The source code of this program is made available
# under the terms of the GNU Affero General Public License version 3
# (GNU AGPL V3) as published by the Free Software Foundation.
#
# Binary versions of this program provided by Univention to you as
# well as other copyrighted, protected or trademarked materials like
# Logos, graphics, fonts, specific documentations and configurations,
# cryptographic keys etc. are subject to a license agreement between
# you and Univention and not subject to the GNU AGPL V3.
#
# In the case you use this program under the terms of the GNU AGPL V3,
# the program is provided 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 with the Debian GNU/Linux or Univention distribution in file
# /usr/share/common-licenses/AGPL-3; if not, see
# <https://www.gnu.org/licenses/>.
"""Implements several helper classes to handle the state of a session
and the communication with the module processes"""
import base64
import ldap
import os
import time
import json
import traceback
import gzip
import re
import errno
import pipes
import six
import ldap.filter
import notifier
import notifier.popen as popen
from notifier import threads
import univention.admin.uexceptions as udm_errors
from .message import Response, Request, MIMETYPE_JSON
from .client import Client, NoSocketError
from .version import VERSION
from .definitions import status_description, SERVER_ERR_MODULE_FAILED, SERVER_ERR_MODULE_DIED
from ..resources import moduleManager, categoryManager
from ..auth import AuthHandler
from ..pam import PamAuth, PasswordChangeFailed
from ..acl import LDAP_ACLs, ACLs
from ..log import CORE
from ..config import MODULE_INACTIVITY_TIMER, MODULE_DEBUG_LEVEL, MODULE_COMMAND, ucr
from ..locales import I18N, I18N_Manager
from ..base import Base
from ..error import UMC_Error, Unauthorized, BadRequest, NotFound, Forbidden, ServiceUnavailable
from ..ldap import get_machine_connection, reset_cache as reset_ldap_connection_cache
from ..modules.sanitizers import StringSanitizer, DictSanitizer
from ..modules.decorators import sanitize, sanitize_args, simple_response, allow_get_request
try:
from typing import Any, Dict, Iterable, List, Optional # noqa: F401
except ImportError:
pass
TEMPUPLOADDIR = '/var/tmp/univention-management-console-frontend'
[docs]class ModuleProcess(Client):
"""handles the communication with a UMC module process
:param str module: name of the module to start
:param str debug: debug level as a string
:param str locale: locale to use for the module process
"""
def __init__(self, module, debug='0', locale=None):
# type: (str, str, str) -> None
socket = '/var/run/univention-management-console/%u-%lu.socket' % (os.getpid(), int(time.time() * 1000))
# determine locale settings
modxmllist = moduleManager[module]
python = '/usr/bin/python3' if any(modxml.python_version == 3 for modxml in modxmllist) else '/usr/bin/python2.7'
args = [python, MODULE_COMMAND, '-m', module, '-s', socket, '-d', str(debug)]
for modxml in modxmllist:
if modxml.notifier:
args.extend(['-n', modxml.notifier])
break
if locale:
args.extend(('-l', '%s' % locale))
self.__locale = locale # type: Optional[str]
else:
self.__locale = None
Client.__init__(self, unix=socket, ssl=False)
self.signal_connect('response', self._response)
CORE.process('running: %s' % ' '.join(pipes.quote(x) for x in args))
self.__process = popen.RunIt(args, stdout=False)
self.__process.signal_connect('killed', self._died)
self.__pid = self.__process.start()
self._connect_retries = 1
self.signal_new('result')
self.signal_new('finished')
self.name = module
self.running = False
self._queued_requests = [] # type: List
self._inactivity_timer = None
self._inactivity_counter = 0
self._connect_timer = None
[docs] def stop(self):
# type: () -> None
CORE.process('ModuleProcess: stopping %r' % (self.__pid,))
notifier.timer_remove(self._connect_timer)
if self.__process:
self.disconnect()
self.__process.signal_disconnect('killed', self._died)
self.__process.stop()
self.__process = None
CORE.info('ModuleProcess: child stopped')
def _died(self, pid, status):
# type: (int, Any) -> None
CORE.process('ModuleProcess: child %d exited with %d' % (pid, status))
self.signal_emit('finished', pid, status)
def _response(self, msg):
# type: (Response) -> None
# these responses must not be send to the external client as
# this commands were generated within the server
if msg.command == 'EXIT' and 'internal' in msg.arguments:
return
self.signal_emit('result', msg)
[docs] def pid(self):
# type: () -> int
"""Returns process ID of module process"""
return self.__pid
[docs]class ProcessorBase(Base):
"""Implements a proxy and command handler. It handles all internal
UMCP commands and passes the commands for a module to the
subprocess.
:param str username: name of the user who authenticated for this session
:param str password: password of the user
"""
@property
def lo(self):
return get_machine_connection(write=False)[0]
def __init__(self):
Base.__init__(self, 'univention-management-console')
self.__processes = {}
self.__killtimer = {}
self.__command_list = None
self.i18n = I18N_Manager()
self.i18n['umc-core'] = I18N()
[docs] def set_locale(self, locale):
# don't call the super method because it sets the process locale LC_*!
self.set_language(locale)
self.i18n.set_locale(locale)
[docs] def set_credentials(self, username, password, auth_type):
self.username = username
self._password = password
self.auth_type = auth_type
self._search_user_dn()
self._reload_acls_and_permitted_commands()
self.update_module_passwords()
def _reload_acls_and_permitted_commands(self):
self._reload_acls()
self.__command_list = moduleManager.permitted_commands(ucr['hostname'], self.acls)
def _reload_acls(self):
try:
self.acls = LDAP_ACLs(self.lo, self._username, ucr['ldap/base'])
except (ldap.LDAPError, udm_errors.ldapError):
reset_ldap_connection_cache()
raise
def _reload_i18n(self):
self.i18n.set_locale(str(self.i18n.locale))
def _search_user_dn(self):
if self.lo and self._username:
# get the LDAP DN of the authorized user
try:
ldap_dn = self.lo.searchDn(ldap.filter.filter_format('(&(uid=%s)(objectClass=person))', (self._username,)))
except (ldap.LDAPError, udm_errors.base):
reset_ldap_connection_cache()
ldap_dn = None
CORE.error('Could not get uid for %r: %s' % (self._username, traceback.format_exc()))
if ldap_dn:
self._user_dn = ldap_dn[0]
CORE.info('The LDAP DN for user %s is %s' % (self._username, self._user_dn))
if not self._user_dn and self._username not in ('root', '__systemsetup__', None):
CORE.error('The LDAP DN for user %s could not be found (lo=%r)' % (self._username, self.lo))
[docs] def error_handling(self, etype, exc, etraceback):
super(ProcessorBase, self).error_handling(etype, exc, etraceback)
if isinstance(exc, UMC_Error) and exc.msg is None:
exc.args = (status_description(exc.status),)
[docs] def request(self, msg):
"""Handles an incoming UMCP request and passes the requests to
specific handler functions.
:param Request msg: UMCP request
"""
if msg.command in ('AUTH', 'EXIT', 'GET', 'SET', 'VERSION', 'COMMAND', 'UPLOAD'):
method = 'handle_request_%s' % (msg.command.lower(),)
else:
method = 'handle_request_unknown'
self.execute(method, msg)
[docs] @allow_get_request
def handle_request_unknown(self, msg):
"""Handles an unknown or invalid request"""
raise NotFound()
[docs] @allow_get_request
def handle_request_unauthorized(self, msg):
raise Unauthorized(self._('For using this request a login is required.'))
[docs] @allow_get_request
def handle_request_auth(self, request):
result = request.authentication_result
del request.authentication_result
response = Response(request)
response.status = result.status
if result.message:
response.message = result.message
response.result = result.result
self.finished(request.id, response)
handle_request_get_ucr = handle_request_unauthorized
handle_request_get_info = handle_request_unauthorized
handle_request_get_user_preferences = handle_request_unauthorized
handle_request_get_hosts = handle_request_unauthorized
handle_request_set_password = handle_request_unauthorized
handle_request_set_locale = handle_request_unauthorized
handle_request_set_user = handle_request_unauthorized
handle_request_version = handle_request_unauthorized
[docs] @allow_get_request
def handle_request_get(self, msg):
"""Handles a GET request"""
for arg in msg.arguments:
method = {
'ucr': self.handle_request_get_ucr,
'meta': self.handle_request_get_meta,
'info': self.handle_request_get_info,
'modules/list': self.handle_request_get_modules,
'modules': self.handle_request_get_modules,
'categories/list': self.handle_request_get_categories,
'categories': self.handle_request_get_categories,
'user/preferences': self.handle_request_get_user_preferences,
'hosts/list': self.handle_request_get_hosts,
'hosts': self.handle_request_get_hosts,
}.get(arg)
if method:
self.finished(msg.id, method(msg))
return
raise NotFound()
META_JSON_PATH = '/var/www/univention/meta.json'
[docs] def handle_request_set(self, msg):
for key, value in msg.options.items():
method = {
'password': self.handle_request_set_password,
'locale': self.handle_request_set_locale,
'user': self.handle_request_set_user,
}.get(key)
if method:
return method(msg)
raise NotFound()
[docs] def handle_request_get_modules(self, request):
categoryManager.load()
moduleManager.load()
if isinstance(request.options, dict) and request.options.get('reload'):
CORE.info('Reloading ACLs for existing session')
self._reload_acls_and_permitted_commands()
self._reload_i18n()
favorites = self._get_user_favorites()
modules = []
for id, module in self.__command_list.items():
# check for translation
if module.flavors:
for flavor in module.flavors:
favcat = []
if '%s:%s' % (id, flavor.id) in favorites:
favcat.append('_favorites_')
translationId = flavor.translationId
if not translationId:
translationId = id
modules.append({
'id': id,
'flavor': flavor.id,
'name': self.i18n._(flavor.name, translationId),
'url': self.i18n._(module.url, translationId),
'description': self.i18n._(flavor.description, translationId),
'icon': flavor.icon,
'categories': (flavor.categories or (module.categories if not flavor.hidden else [])) + favcat,
'priority': flavor.priority,
'keywords': list(set(flavor.keywords + [self.i18n._(keyword, translationId) for keyword in flavor.keywords])),
'version': flavor.version,
})
else:
favcat = []
if id in favorites:
favcat.append('_favorites_')
translationId = module.translationId
if not translationId:
translationId = id
modules.append({
'id': id,
'name': self.i18n._(module.name, translationId),
'url': self.i18n._(module.url, translationId),
'description': self.i18n._(module.description, translationId),
'icon': module.icon,
'categories': module.categories + favcat,
'priority': module.priority,
'keywords': list(set(module.keywords + [self.i18n._(keyword, translationId) for keyword in module.keywords])),
'version': module.version,
})
CORE.info('Modules: %s' % (modules,))
res = Response(request)
res.body['modules'] = modules
return res
def _get_user_favorites(self):
if not self._user_dn: # user not authenticated or no LDAP user
return set(ucr.get('umc/web/favorites/default', '').split(','))
favorites = self._get_user_preferences(self.get_user_ldap_connection(no_cache=True)).setdefault('favorites', ucr.get('umc/web/favorites/default', '')).strip()
return set(favorites.split(','))
[docs] def handle_request_get_categories(self, request):
categoryManager.load()
ucr.load()
_ucr_dict = dict(ucr.items())
categories = []
for catID, category in categoryManager.items():
categories.append({
'id': catID,
'icon': category.icon,
'color': category.color,
'name': self.i18n._(category.name, category.domain).format(**_ucr_dict),
'priority': category.priority
})
CORE.info('Categories: %s' % (categories,))
res = Response(request)
res.body['categories'] = categories
return res
[docs] @sanitize(locale=StringSanitizer(required=True))
@simple_response
def handle_request_set_locale(self, locale):
self.update_language([locale])
[docs] def update_module_passwords(self):
if self.__processes:
CORE.process('Updating user password in %d running module processes (auth-type: %s).' % (len(self.__processes), self.auth_type))
for module_name, proc in self.__processes.items():
CORE.info('Update the users password in the running %r module instance.' % (module_name,))
req = Request('SET', arguments=[module_name], options={'password': self._password, 'auth_type': self.auth_type})
try:
proc.request(req)
except Exception:
CORE.error(traceback.format_exc())
[docs] @allow_get_request
@sanitize(DictSanitizer(dict(
tmpfile=StringSanitizer(required=True),
filename=StringSanitizer(required=True),
name=StringSanitizer(required=True),
)))
def handle_request_upload(self, msg):
"""Handles an UPLOAD request. The command is used for the HTTP
access to the UMC server. Incoming HTTP requests that send a
list of files are passed on to the UMC server by storing the
files in temporary files and passing the information about the
files to the UMC server in the options of the request. The
request options must be a list of dictionaries. Each dictionary
must contain the following keys:
* *filename* -- the original name of the file
* *name* -- name of the form field
* *tmpfile* -- filename of the temporary file
:param Request msg: UMCP request
"""
direct_response = not msg.arguments or msg.arguments[0] in ('', '/')
result = []
for file_obj in msg.options:
tmpfilename, filename, name = file_obj['tmpfile'], file_obj['filename'], file_obj['name']
# limit files to tmpdir
if not os.path.realpath(tmpfilename).startswith(TEMPUPLOADDIR):
raise BadRequest('invalid file: invalid path')
# check if file exists
if not os.path.isfile(tmpfilename):
raise BadRequest('invalid file: file does not exists')
# don't accept files bigger than umc/server/upload/max
st = os.stat(tmpfilename)
max_size = int(ucr.get('umc/server/upload/max', 64)) * 1024
if st.st_size > max_size:
os.remove(tmpfilename)
raise BadRequest('filesize is too large, maximum allowed filesize is %d' % (max_size,))
if direct_response:
with open(tmpfilename, 'rb') as buf:
b64buf = base64.b64encode(buf.read()).decode('ASCII')
result.append({'filename': filename, 'name': name, 'content': b64buf})
if direct_response:
self.finished(msg.id, result)
else:
self.handle_request_command(msg)
[docs] @allow_get_request
def handle_request_command(self, msg):
"""Handles a COMMAND request. The request must contain a valid
and known command that can be accessed by the current user. If
access to the command is prohibited the request is answered as a
forbidden command.
If there is no running module process for the given command a
new one is started and the request is added to a queue of
requests that will be passed on when the process is ready.
If a module process is already running the request is passed on
and the inactivity timer is reset.
:param Request msg: UMCP request
"""
# only one command?
command = None
if msg.arguments:
command = msg.arguments[0]
module_name = moduleManager.module_providing(self.__command_list, command)
try:
# check if the module exists in the module manager
moduleManager[module_name]
except KeyError:
# the module has been removed from moduleManager (probably through a reload)
CORE.warn('Module %r (command=%r, id=%r) does not exists anymore' % (module_name, command, msg.id))
moduleManager.load()
self._reload_acls_and_permitted_commands()
module_name = None
if not module_name:
raise Forbidden()
if msg.arguments:
if msg.mimetype == MIMETYPE_JSON:
is_allowed = moduleManager.is_command_allowed(self.acls, msg.arguments[0], options=msg.options, flavor=msg.flavor)
else:
is_allowed = moduleManager.is_command_allowed(self.acls, msg.arguments[0])
if not is_allowed:
raise Forbidden()
if module_name not in self.__processes:
CORE.info('Starting new module process and passing new request to module %s: %s' % (module_name, str(msg._id)))
try:
mod_proc = ModuleProcess(module_name, debug=MODULE_DEBUG_LEVEL, locale=self.i18n.locale)
except EnvironmentError as exc:
message = self._('Could not open the module. %s Please try again later.') % {
errno.ENOMEM: self._('There is not enough memory available on the server.'),
errno.EMFILE: self._('There are too many opened files on the server.'),
errno.ENFILE: self._('There are too many opened files on the server.'),
errno.ENOSPC: self._('There is not enough free space on the server.')
}.get(exc.errno, self._('An unknown operating system error occurred (%s).' % (exc,)))
raise ServiceUnavailable(message)
mod_proc.signal_connect('result', self.result)
cb = notifier.Callback(self._mod_error, module_name)
mod_proc.signal_connect('error', cb)
cb = notifier.Callback(self._socket_died, module_name)
mod_proc.signal_connect('closed', cb)
cb = notifier.Callback(self._mod_died, module_name)
mod_proc.signal_connect('finished', cb)
self.__processes[module_name] = mod_proc
cb = notifier.Callback(self._mod_connect, mod_proc, msg)
mod_proc._connect_timer = notifier.timer_add(50, cb)
else:
proc = self.__processes[module_name]
if proc.running:
CORE.info('Passing new request to running module %s' % module_name)
proc.request(msg)
self.reset_inactivity_timer(proc)
else:
CORE.info('Queuing incoming request for module %s that is not yet ready to receive' % module_name)
proc._queued_requests.append(msg)
def _mod_connect(self, mod, msg):
"""Callback for a timer event: Trying to connect to newly started module process"""
def _send_error():
# inform client
res = Response(msg)
res.status = SERVER_ERR_MODULE_FAILED # error connecting to module process
res.message = '%s: %s' % (status_description(res.status), mod.name)
self.result(res)
# cleanup module
mod.signal_disconnect('closed', notifier.Callback(self._socket_died))
mod.signal_disconnect('result', notifier.Callback(self.result))
mod.signal_disconnect('finished', notifier.Callback(self._mod_died))
proc = self.__processes.pop(mod.name, None)
if proc:
proc.stop()
try:
mod.connect()
except NoSocketError:
if mod._connect_retries > 200:
CORE.info('Connection to module %s process failed' % mod.name)
_send_error()
return False
if not mod._connect_retries % 50:
CORE.info('No connection to module process yet')
mod._connect_retries += 1
return True
except Exception as exc:
CORE.error('Unknown error while trying to connect to module process: %s\n%s' % (exc, traceback.format_exc()))
_send_error()
return False
else:
CORE.info('Connected to new module process')
mod.running = True
# send acls, commands, credentials, locale
options = {
'acls': self.acls.json(),
'commands': self.__command_list[mod.name].json(),
'credentials': {
'auth_type': self.auth_type,
'username': self._username,
'password': self._password,
'user_dn': self._user_dn
},
}
if str(self.i18n.locale):
options['locale'] = str(self.i18n.locale)
# WARNING! This debug message contains credentials!!!
# CORE.info('Initialize module process: %s' % (options,))
req = Request('SET', options=options)
mod.request(req)
# send first command
mod.request(msg)
# send queued request that were received during start procedure
for req in mod._queued_requests:
mod.request(req)
mod._queued_requests = []
# watch the module's activity and kill it after X seconds inactivity
self.reset_inactivity_timer(mod)
return False
def _mod_inactive(self, module):
CORE.info('The module %s is inactive for too long. Sending EXIT request to module' % module.name)
if module.openRequests:
CORE.info('There are unfinished requests. Waiting for %s' % ', '.join(module.openRequests))
return True
# mark as internal so the response will not be forwarded to the client
req = Request('EXIT', arguments=[module.name, 'internal'])
self.handle_request_exit(req)
return False
def _socket_died(self, module_name):
CORE.warn('Socket died (module=%s)' % module_name)
if module_name in self.__processes:
self._mod_died(self.__processes[module_name].pid(), -1, module_name)
def _mod_error(self, exc, module_name):
CORE.error('Module %r ran into error: %s' % (module_name, exc))
if module_name in self.__processes:
self.__processes[module_name].invalidate_all_requests(status=exc.args[0], message=exc.args[1])
self._mod_died(self.__processes[module_name].pid(), -1, module_name)
self._purge_child(module_name)
def _mod_died(self, pid, status, module_name):
if status:
if os.WIFSIGNALED(status):
signal = os.WTERMSIG(status)
exitcode = -1
elif os.WIFEXITED(status):
signal = -1
exitcode = os.WEXITSTATUS(status)
else:
signal = -1
exitcode = -1
CORE.warn('Module process %s died (pid: %d, exit status: %d, signal: %d, status: %r)' % (module_name, pid, exitcode, signal, status))
else:
CORE.info('Module process %s died on purpose' % module_name)
# if killtimer has been set then remove it
CORE.info('Checking for kill timer (%s)' % ', '.join(self.__killtimer.keys()))
if module_name in self.__killtimer:
CORE.info('Stopping kill timer)')
notifier.timer_remove(self.__killtimer[module_name])
del self.__killtimer[module_name]
if module_name in self.__processes:
CORE.warn('Cleaning up requests')
self.__processes[module_name].invalidate_all_requests(status=SERVER_ERR_MODULE_DIED)
if self.__processes[module_name]._inactivity_timer is not None:
CORE.warn('Remove inactivity timer')
notifier.timer_remove(self.__processes[module_name]._inactivity_timer)
self.__processes.pop(module_name).stop()
[docs] def reset_inactivity_timer(self, module):
"""Resets the inactivity timer. This timer watches the
inactivity of the module process. If the module did not receive
a request for MODULE_INACTIVITY_TIMER seconds the module process
is shut down to save resources. The timer ticks each seconds to
handle glitches of the system clock.
:param Module module: a UMC module
"""
if module._inactivity_timer is None:
module._inactivity_timer = notifier.timer_add(1000, notifier.Callback(self._inactivitiy_tick, module))
module._inactivity_counter = MODULE_INACTIVITY_TIMER
def _inactivitiy_tick(self, module):
if module._inactivity_counter > 0:
module._inactivity_counter -= 1000
return True
if self._mod_inactive(module): # open requests -> waiting
module._inactivity_counter = MODULE_INACTIVITY_TIMER
return True
module._inactivity_timer = None
module._inactivity_counter = 0
return False
[docs] def handle_request_exit(self, msg):
"""Handles an EXIT request. If the request does not have an
argument that contains a valid name of a running UMC module
instance the request is returned as a bad request.
If the request is valid it is passed on to the module
process. Additionally a timer of 3000 milliseconds is
started. After that amount of time the module process MUST have
been exited itself. If not the UMC server will kill the module
process.
:param Request msg: UMCP request
"""
if len(msg.arguments) < 1:
return self.handle_request_unknown(msg)
module_name = msg.arguments[0]
if module_name:
if module_name in self.__processes:
self.__processes[module_name].request(msg)
CORE.info('Ask module %s to shutdown gracefully' % module_name)
# added timer to kill away module after 3000ms
cb = notifier.Callback(self._purge_child, module_name)
self.__killtimer[module_name] = notifier.timer_add(3000, cb)
else:
CORE.info('Got EXIT request for a non-existing module %s' % module_name)
def _purge_child(self, module_name):
if module_name in self.__processes:
CORE.process('module %s is still running - purging module out of memory' % module_name)
pid = self.__processes[module_name].pid()
try:
os.kill(pid, 9)
except OSError as exc:
CORE.warn('Failed to kill module %s: %s' % (module_name, exc))
return False
[docs] def shutdown(self):
"""Instructs the module process to shutdown"""
if self.__processes:
CORE.info('The session is shutting down. Sending EXIT request to %d modules.' % len(self.__processes))
for module_name in list(self.__processes.keys()):
CORE.info('Ask module %s to shutdown gracefully' % (module_name,))
req = Request('EXIT', arguments=[module_name, 'internal'])
process = self.__processes.pop(module_name)
process.request(req)
notifier.timer_remove(process._connect_timer)
notifier.timer_add(4000, process.stop)
if self._user_connections:
reset_ldap_connection_cache(*self._user_connections)
if isinstance(self.acls, LDAP_ACLs):
reset_ldap_connection_cache(self.acls.lo)
self.acls = None
[docs]class Processor(ProcessorBase):
[docs] @sanitize(StringSanitizer(required=True))
def handle_request_get_ucr(self, request):
ucr.load()
result = {}
for value in request.options:
if value.endswith('*'):
value = value[:-1]
result.update(dict((x, ucr.get(x)) for x in ucr.keys() if x.startswith(value)))
else:
result[value] = ucr.get(value)
return result
META_UCR_VARS = [
'domainname',
'hostname',
'ldap/master',
'license/base',
'server/role',
'ssl/validity/host',
'ssl/validity/root',
'ssl/validity/warning',
'umc/web/favorites/default',
'umc/web/piwik',
'update/available',
'update/reboot/required',
'uuid/license',
'uuid/system',
'version/erratalevel',
'version/patchlevel',
'version/version',
]
CHANGELOG_VERSION = re.compile(r'^[^(]*\(([^)]*)\).*')
[docs] def handle_request_get_info(self, request):
ucr.load()
result = {}
try:
with gzip.open('/usr/share/doc/univention-management-console-server/changelog.Debian.gz') as fd:
line = fd.readline().decode('utf-8', 'replace')
match = self.CHANGELOG_VERSION.match(line)
if not match:
raise IOError
result['umc_version'] = match.groups()[0]
result['ucs_version'] = '%(version/version)s-%(version/patchlevel)s errata%(version/erratalevel)s' % ucr
result['server'] = '{0}.{1}'.format(ucr.get('hostname', ''), ucr.get('domainname', ''))
result['ssl_validity_host'] = int(ucr.get('ssl/validity/host', '0')) * 24 * 60 * 60 * 1000
result['ssl_validity_root'] = int(ucr.get('ssl/validity/root', '0')) * 24 * 60 * 60 * 1000
except IOError:
raise Forbidden()
return result
[docs] def handle_request_get_hosts(self, request):
result = []
if self.lo:
try:
domaincontrollers = self.lo.search(filter="(objectClass=univentionDomainController)", attr=['cn', 'associatedDomain'])
except (ldap.LDAPError, udm_errors.base) as exc:
reset_ldap_connection_cache()
CORE.warn('Could not search for domaincontrollers: %s' % (exc))
domaincontrollers = []
result = sorted([(b'%s.%s' % (computer['cn'][0], computer['associatedDomain'][0])).decode('utf-8', 'replace') for dn, computer in domaincontrollers if computer.get('associatedDomain')])
return result
[docs] @sanitize(password=DictSanitizer(dict(
password=StringSanitizer(required=True),
new_password=StringSanitizer(required=True),
)))
def handle_request_set_password(self, request):
username = self._username
password = request.options['password']['password']
new_password = request.options['password']['new_password']
CORE.info('Changing password of user %r' % (username,))
pam = PamAuth(str(self.i18n.locale))
change_password = notifier.Callback(pam.change_password, username, password, new_password)
password_changed = notifier.Callback(self._password_changed, request, new_password)
thread = threads.Simple('change_password', change_password, password_changed)
thread.run()
def _password_changed(self, thread, result, request, new_password):
# it is important that this thread callback must not raise an exception. Otherwise the UMC-Server crashes.
if isinstance(result, PasswordChangeFailed):
self.finished(request.id, {'new_password': '%s' % (result,)}, message=str(result), status=400) # 422
elif isinstance(result, BaseException):
self.thread_finished_callback(thread, result, request)
else:
CORE.info('Successfully changed password')
self.finished(request.id, None, message=self._('Password successfully changed.'))
self.auth_type = None
self._password = new_password
self.update_module_passwords()
[docs] def handle_request_get_user_preferences(self, request):
# fallback is an empty dict
res = Response(request)
res.body['preferences'] = self._get_user_preferences(self.get_user_ldap_connection())
return res
[docs] @sanitize(user=DictSanitizer(dict(
preferences=DictSanitizer(dict(), required=True),
)))
@simple_response
def handle_request_set_user(self, user):
lo = self.get_user_ldap_connection()
# eliminate double entries
preferences = self._get_user_preferences(lo)
preferences.update(dict(user['preferences']))
if preferences:
self._set_user_preferences(lo, preferences)
def _get_user_preferences(self, lo):
if not self._user_dn or not lo:
return {}
try:
preferences = lo.get(self._user_dn, ['univentionUMCProperty']).get('univentionUMCProperty', [])
except (ldap.LDAPError, udm_errors.base) as exc:
CORE.warn('Failed to retrieve user preferences: %s' % (exc,))
return {}
preferences = (val.decode('utf-8', 'replace') for val in preferences)
return dict(val.split(u'=', 1) if u'=' in val else (val, u'') for val in preferences)
def _set_user_preferences(self, lo, preferences):
if not self._user_dn or not lo:
return
user = lo.get(self._user_dn, ['univentionUMCProperty', 'objectClass'])
old_preferences = user.get('univentionUMCProperty')
object_classes = list(set(user.get('objectClass', [])) | set([b'univentionPerson']))
# validity / sanitizing
new_preferences = []
for key, value in preferences.items():
if not isinstance(key, six.string_types):
CORE.warn('user preferences keys needs to be strings: %r' % (key,))
continue
# we can put strings directly into the dict
if isinstance(value, six.string_types):
new_preferences.append((key, value))
else:
new_preferences.append((key, json.dumps(value)))
new_preferences = [b'%s=%s' % (key.encode('utf-8'), value.encode('utf-8')) for key, value in new_preferences]
lo.modify(self._user_dn, [['univentionUMCProperty', old_preferences, new_preferences], ['objectClass', user.get('objectClass', []), object_classes]])
[docs] def handle_request_version(self, msg):
"""Handles a VERSION request by returning the version of the UMC
server's protocol version.
:param Request msg: UMCP request
"""
res = Response(msg)
res.body['version'] = VERSION
self.finished(msg.id, res)
[docs]class SessionHandler(ProcessorBase):
def __init__(self):
super(SessionHandler, self).__init__()
self.__auth = AuthHandler()
self.__auth.signal_connect('authenticated', self._authentication_finished)
self.processor = None
self.authenticated = False
self.__credentials = None
self.__locale = None
self._reload_acls_and_permitted_commands()
[docs] def has_active_module_processes(self):
if self.processor:
return self.processor._ProcessorBase__processes
def _reload_acls(self):
"""All unauthenticated requests are passed here. We need to set empty ACL's"""
self.acls = ACLs()
[docs] def error_handling(self, etype, exc, etraceback):
super(SessionHandler, self).error_handling(etype, exc, etraceback)
# make sure that the UMC login dialog is shown if e.g. restarting the UMC-Server during active sessions
if isinstance(exc, UMC_Error) and exc.status == 403:
exc.status = 401
def _authentication_finished(self, result, request):
# caution! this is not executed in the main loop and any exception will therefore crash the server!
self.execute('_authentication_finished2', request, result)
@allow_get_request
def _authentication_finished2(self, request, result):
self.authenticated = bool(result)
request.authentication_result = result
if self.authenticated:
if self.processor is None or self.processor.auth_type is not None or result.credentials['auth_type'] is None:
# only set the credentials in 1. a new session 2. if password changed or 3. if logged in via plain authentication
# to prevent a downgrade of the regular login to a SAML login
self.__credentials = result.credentials
if self.processor:
# set the (new) password (also on re-authentication in the same session)
self.processor.set_credentials(**self.__credentials)
else:
self.initalize_processor(request)
self.processor.request(request)
else:
self.request(request)
[docs] @allow_get_request
def handle(self, request):
"""Ensures that commands are only passed to the processor if a
successful authentication has been completed."""
CORE.info('Incoming request of type %s' % (request.command,))
if not self.authenticated and request.command != 'AUTH':
self.request(request)
elif request.command == 'AUTH':
self._handle_auth(request)
elif request.command == 'GET' and 'newsession' in request.arguments:
CORE.info('Renewing session')
if self.processor:
self.__locale = str(self.processor.locale)
self.processor = None
self.finished(request.id, None)
else:
self.initalize_processor(request)
self.processor.request(request)
def _handle_auth(self, request):
request.body = sanitize_args(DictSanitizer(dict(
username=StringSanitizer(required=True),
password=StringSanitizer(required=True),
auth_type=StringSanitizer(allow_none=True),
new_password=StringSanitizer(required=False, allow_none=True),
)), 'request', {'request': request.body})
from univention.management.console.protocol.server import Server
Server.reload()
request.body['locale'] = str(self.i18n.locale)
self.__auth.authenticate(request)
[docs] def initalize_processor(self, request):
if not self.processor:
self.processor = Processor()
self.processor.signal_connect('success', self._response)
if self.__locale:
self.processor.update_language([self.__locale])
self.processor.set_credentials(**self.__credentials)
def _response(self, response):
self.signal_emit('success', response)
[docs] @allow_get_request
def parse_error(self, request, parse_error):
status, message = parse_error.args
raise UMC_Error(message, status=status)
[docs] def close_session(self):
self.__auth.signal_disconnect('authenticated', self._authentication_finished)
self.shutdown()
if self.processor is not None:
self.processor.shutdown()
self.processor = None