Source code for univention.management.console.modules.passwordreset

#!/usr/bin/python3
#
# Univention Management Console
#  module: password reset service
#
# SPDX-FileCopyrightText: 2015-2026 Univention GmbH
# SPDX-License-Identifier: AGPL-3.0-only

import base64
import binascii
import datetime as dt
import email.charset
import os.path
import random
import smtplib
import string
import time
from email.mime.nonmultipart import MIMENonMultipart
from email.utils import formatdate
from functools import wraps
from subprocess import PIPE, STDOUT, Popen
from typing import Any, NamedTuple

import atexit
import pylibmc
from ldap.filter import filter_format

import univention.admin.modules
import univention.admin.syntax as udm_syntax
import univention.admin.uexceptions
import univention.admin.uexceptions as udm_errors
import univention.admin.uldap
from univention.admin.uldap import getMachineConnection
from univention.lib import icap
from univention.lib.i18n import Translation
from univention.lib.umc import Client, ConnectionError, HTTPError, Unauthorized  # noqa: A004
from univention.management.console.config import ucr
from univention.management.console.ldap import (
    get_admin_connection, get_machine_connection, get_user_connection, machine_connection,
)
from univention.management.console.log import MODULE
from univention.management.console.modules import Base, UMC_Error
from univention.management.console.modules.decorators import SimpleThread, sanitize, simple_response
from univention.management.console.modules.sanitizers import DictSanitizer, StringSanitizer

from .sending import get_plugins as get_sending_plugins
from .tokendb import MultipleTokensInDB, TokenDB


_ = Translation('univention-self-service-passwordreset-umc').translate

MEMCACHED_SOCKET = ucr.get("umc/self-service/memcached/socket", "/var/lib/univention-self-service-passwordreset-umc/memcached.socket")
MEMCACHED_USERNAME = ucr.get("umc/self-service/memcached/username")
MEMCACHED_SECRET_FILE = ucr.get("umc/self-service/memcached/password-file")
MEMCACHED_MAX_KEY = 250

SELFSERVICE_MASTER = ucr.get("self-service/backend-server", ucr.get("ldap/master"))
IS_SELFSERVICE_MASTER = '%(hostname)s.%(domainname)s' % ucr == SELFSERVICE_MASTER
UDM_REST_SERVER = ucr.get('self-service/udm-rest-server', '%(hostname)s.%(domainname)s' % ucr)
DISALLOW_AUTHENTICATION = not ucr.is_true('umc/self-service/allow-authenticated-use')

DEREGISTRATION_TIMESTAMP_FORMATTING = '%Y%m%d%H%M%SZ'
# Only add syntax classes whose parse() requires Base64 and decodes the value with base64.b64decode().
# uploaded_files() decodes the values the same way to scan them. A syntax class that stores values
# without Base64 would let the scan see other bytes than the ones that get stored in LDAP.
UPLOAD_SYNTAXES = (udm_syntax.jpegPhoto, udm_syntax.Base64Upload, udm_syntax.Base64BaseUpload)
MAX_UPLOAD_FILES = 10

if IS_SELFSERVICE_MASTER:
    try:
        from univention.admin.rest.client import UDM as UDMRest
        from univention.management.console.modules.udm.udm_ldap import UDM_Error, UDM_Module
        from univention.udm import UDM, NoObject
    except ImportError as exc:
        MODULE.error('Could not load udm module: %s', exc)


[docs] def forward_to_master(func): @wraps(func) def _decorator(self, request, *args, **kwargs): if not IS_SELFSERVICE_MASTER: try: language = str(self.locale).split('.')[0].replace('_', '-') client = Client(SELFSERVICE_MASTER, language=language) client.authenticate_with_machine_account() response = client.umc_command(request.arguments[0], request.options) except (Unauthorized, ConnectionError) as exc: raise UMC_Error(_('The connection to the server could not be established. Please try again later. Error message was: %s') % (exc,), status=503) except HTTPError as exc: response = exc.response self.finished(request.id, response.result, message=response.message, status=response.status) return return func(self, request, *args, **kwargs) return _decorator
[docs] def forward_to_master_if_authentication_disabled(func): if DISALLOW_AUTHENTICATION: return forward_to_master(func) return func
[docs] class ScanSettings(NamedTuple): """Settings of the malware scan, see :func:`malware_scan_settings`.""" url: str timeout: int cafile: str | None max_files: int
[docs] def scan_files_for_malware(files: list[tuple[str, bytes]], settings: ScanSettings, username: str | None) -> None: """ Scan uploaded files with the ICAP server. This runs in a worker thread. The function only gets prepared data, see :func:`uploaded_files` and :func:`malware_scan_settings`, so that it doesn't touch shared state of the module. All scans of one request together must finish within the configured timeout. :param list files: Pairs of property name and decoded file content. :param ScanSettings settings: The URL, the timeout and the CA file of the ICAP server. :param str username: The name of the user, for the log messages. :raises UMC_Error: If the ICAP server finds malware or cannot scan a file. """ url, timeout, cafile, _max_files = settings deadline = time.monotonic() + max(timeout, 0) for propname, data in files: try: remaining = deadline - time.monotonic() if remaining <= 0: raise icap.ICAPError('The malware scan took longer than %d seconds' % (timeout,)) result = icap.scan(data, url, timeout=remaining, filename=propname, cafile=cafile) except (icap.ICAPError, ValueError) as exc: MODULE.error('Malware scan of attribute %s of user %s failed: %s', propname, username, exc) raise UMC_Error(_('Upload failed: Malware scan not currently possible. Please try again later or contact an administrator.'), status=503) if result.infected: MODULE.warning('Malware scan rejected attribute %s of user %s: %s', propname, username, result.threat) raise UMC_Error(_('Upload rejected: malware was detected in the file.'))
[docs] def malware_scan_settings() -> ScanSettings: """ Get the settings of the malware scan: the URL, the timeout and the CA file of the ICAP server and the maximum number of files in one request. The environment variables `SELF_SERVICE_MALWARE_SCAN_ICAP_URL`, `SELF_SERVICE_MALWARE_SCAN_ICAP_TIMEOUT`, `SELF_SERVICE_MALWARE_SCAN_ICAP_CA_FILE` and `SELF_SERVICE_MALWARE_SCAN_MAX_FILES` have priority over the UCR variables `umc/self-service/malware-scan/icap/url`, `.../icap/timeout`, `.../icap/ca-file` and `.../max-files`. Empty or invalid environment variables are ignored. :returns: The settings. The URL is empty if the scan is disabled, the CA file is `None` if the CA certificates of the system are used. """ url = os.environ.get('SELF_SERVICE_MALWARE_SCAN_ICAP_URL') or ucr.get('umc/self-service/malware-scan/icap/url', '') cafile = os.environ.get('SELF_SERVICE_MALWARE_SCAN_ICAP_CA_FILE') or ucr.get('umc/self-service/malware-scan/icap/ca-file') or None timeout = _env_int('SELF_SERVICE_MALWARE_SCAN_ICAP_TIMEOUT', ucr.get_int('umc/self-service/malware-scan/icap/timeout', 30)) max_files = _env_int('SELF_SERVICE_MALWARE_SCAN_MAX_FILES', ucr.get_int('umc/self-service/malware-scan/max-files', MAX_UPLOAD_FILES)) return ScanSettings(url, timeout, cafile, max(max_files, 0))
def _env_int(name: str, default: int) -> int: """ Get a non-negative integer from an environment variable. :param str name: The name of the environment variable. :param int default: The value if the environment variable is unset, empty or invalid. :returns: The value. """ value = os.environ.get(name) if not value: return default try: number = int(value) if number < 0: raise ValueError(number) except ValueError: MODULE.warning('Ignoring invalid value %r of %s', value, name) return default return number
[docs] def upload_values(property_descriptions: dict[str, Any], attributes: dict[str, Any]) -> list[tuple[str, Any]]: """ Collect the non-empty values of the attributes with an upload syntax from :data:`UPLOAD_SYNTAXES`, for example `jpegPhoto`, without decoding them. Multivalue attributes, also extended attributes, give one entry per value. :param dict property_descriptions: The UDM property descriptions of the `users/user` module. :param dict attributes: The attribute values that the user sent. :returns: Pairs of property name and submitted value. """ items = [] for propname, value in attributes.items(): prop = property_descriptions.get(propname) if not prop or not isinstance(prop.syntax, UPLOAD_SYNTAXES): continue items.extend((propname, item) for item in (value if isinstance(value, list | tuple) else [value]) if item) return items
[docs] def check_upload_limit(property_descriptions: dict[str, Any], attributes: dict[str, Any], settings: ScanSettings) -> None: """ Reject a request with more uploaded files than allowed, before any file is decoded, validated or scanned. The limit only applies when the malware scan is enabled. All non-empty values of all upload properties count, also values that are not valid Base64. :param dict property_descriptions: The UDM property descriptions of the `users/user` module. :param dict attributes: The attribute values that the user sent. :param ScanSettings settings: The settings of the malware scan, `max_files` is the limit, `0` allows no file. :raises UMC_Error: If the request contains more files than allowed. """ if not settings.url: return count = len(upload_values(property_descriptions, attributes)) if count > settings.max_files: MODULE.warning('Upload rejected: %d files, at most %d are allowed', count, settings.max_files) raise UMC_Error(_('Upload rejected: too many files. You can upload at most %d files at once.') % (settings.max_files,))
[docs] def uploaded_files(property_descriptions: dict[str, Any], attributes: dict[str, Any]) -> list[tuple[str, bytes]]: """ Decode the uploaded files in the attributes, see :func:`upload_values`. Values that are not Base64 are skipped, UDM rejects them later. Call :func:`check_upload_limit` first. :param dict property_descriptions: The UDM property descriptions of the `users/user` module. :param dict attributes: The attribute values that the user sent. :returns: Pairs of property name and decoded file content. """ files = [] for propname, item in upload_values(property_descriptions, attributes): if not isinstance(item, str): continue try: data = base64.b64decode(item) except (binascii.Error, ValueError): # FIXME: this requires all syntax classes to be base64 otherwise it would be vulnerable! See comment at UPLOAD_SYNTAXES. continue if data: files.append((propname, data)) return files
[docs] def prevent_denial_of_service(func): def _pretty_time(sec): if sec <= 60: return _("one minute") m, _s = divmod(sec, 60) if m < 60: return _("{} minutes").format(m + 1) elif m == 60: return _("one hour") # and one minute, but nvm h, m = divmod(m, 60) return _("{} hours").format(h + 1) def _check_limits(memcache, limits): limit_reached = False _max_wait = dt.datetime.utcnow() for key, decay, limit in limits: # Not really a "decay", as for that we'd have to store the date for # each request. Then a moving window could be implemented. But # my guess is that we won't need that, so this is simpler. # Continue even if a limit was reached, so that all counters are # incremented. if limit == 0: # limit deactivated by UCR continue try: count = memcache.incr(key) except pylibmc.NotFound: count = 1 memcache.set_multi( { key: count, f"{key}:exp": dt.datetime.utcnow() + dt.timedelta(seconds=decay), }, decay, ) if count > limit: limit_reached = True _max_wait = max(_max_wait, memcache.get(f"{key}:exp")) return limit_reached, _max_wait @wraps(func) def _decorated(self, *args, **kwargs): request = self._current_request client_host_str = None # Determine the client host. Respect X-Forwarded-Host if configured and present. x_forwarded_host = request.headers.get('X-Forwarded-Host') if x_forwarded_host: # Take the first Host in the list (closest client) client_host_str = x_forwarded_host.split(',')[0].strip() MODULE.debug("Rate limit check: Using X-Forwarded-Host: %s", client_host_str) else: MODULE.debug("Rate limit check: Cannot determine remote Host from request object.") if client_host_str and client_host_str in self.trusted_hosts: MODULE.debug("Rate limit bypassed for trusted host %s in trusted hosts %r", client_host_str, self.trusted_hosts) return func(self, *args, **kwargs) # check total request limits total_limit_reached, total_max_wait = _check_limits(self.memcache, self.total_limits) # check user request limits try: if "username" in kwargs: username = kwargs["username"] else: username = args[0].options["username"] except (IndexError, AttributeError, KeyError, TypeError): # args[0] is not the expected 'request' MODULE.exception("prevent_denial_of_service() could not find username argument. self: %r args: %r kwargs: %r", self, args, kwargs) raise # TODO: return func(self, *args, **kwargs) here?! if len(username) > MEMCACHED_MAX_KEY - 9: # "_hour:exp" raise ServiceForbidden() username = self.email2username(username) user_limits = [ (f"{username}_minute", 60, self.limit_user_minute), (f"{username}_hour", 3600, self.limit_user_hour), (f"{username}_day", 86400, self.limit_user_day), ] user_limit_reached, user_max_wait = _check_limits(self.memcache, user_limits) if total_limit_reached or user_limit_reached: time_s = _pretty_time((max(total_max_wait, user_max_wait) - dt.datetime.utcnow()).total_seconds()) raise ConnectionLimitReached(time_s) return func(self, *args, **kwargs) return _decorated
[docs] class ConnectionLimitReached(UMC_Error): status = 503 def __init__(self, seconds): super().__init__(_("The allowed maximum number of connections to the server has been reached. Please retry in {}.").format(seconds))
[docs] class ServiceForbidden(UMC_Error): # protection against bruteforcing user names status = 403 def __init__(self): super().__init__(_("Either username or password is incorrect or you are not allowed to use this service."))
[docs] class TokenNotFound(UMC_Error): status = 400 def __init__(self): super().__init__( _("The token you supplied is either expired or invalid. Please request a new one."))
[docs] class NoMethodsAvailable(UMC_Error): status = 403 def __init__(self): super().__init__(_('No contact information is stored for this user. Resetting the password is not possible.'))
[docs] class TokenSendMessage(UMC_Error): status = 200 def __init__(self): super().__init__(_("A message containing a token has been sent to the user (if the user exists and is allowed to use this service)."))
[docs] class Instance(Base):
[docs] def init(self): if not ucr.is_true("umc/self-service/enabled"): raise UMC_Error(_('The password reset service is disabled via configuration registry.'), status=503) self._usersmod = None self.groupmod = None self.trusted_hosts = [ h.strip() for h in ucr.get('umc/self-service/rate-limit/trusted-hosts', '').split(',') if h.strip() ] MODULE.info("Added trusted host for rate limit bypass: %r", self.trusted_hosts) self.token_validity_period = ucr.get_int("umc/self-service/passwordreset/token_validity_period", 3600) limit_total_minute = ucr.get_int("umc/self-service/passwordreset/limit/total/minute", 0) limit_total_hour = ucr.get_int("umc/self-service/passwordreset/limit/total/hour", 0) limit_total_day = ucr.get_int("umc/self-service/passwordreset/limit/total/day", 0) self.limit_user_minute = ucr.get_int("umc/self-service/passwordreset/limit/per_user/minute", 0) self.limit_user_hour = ucr.get_int("umc/self-service/passwordreset/limit/per_user/hour", 0) self.limit_user_day = ucr.get_int("umc/self-service/passwordreset/limit/per_user/day", 0) self.total_limits = [ ("t:c_minute", 60, limit_total_minute), ("t:c_hour", 3600, limit_total_hour), ("t:c_day", 86400, limit_total_day), ] if IS_SELFSERVICE_MASTER: self.db = TokenDB(MODULE) atexit.register(self.db.close_db) if not self.db.table_exists(): self.db.create_table() password = os.getenv("SELF_SERVICE_MEMCACHED_SECRET") if not password and MEMCACHED_SECRET_FILE: try: with open(MEMCACHED_SECRET_FILE) as pw_file: password = pw_file.readline().strip() except OSError: raise UMC_Error('The memcached password is not properly configured.', status=503) self.memcache = pylibmc.Client([MEMCACHED_SOCKET], binary=True, username=MEMCACHED_USERNAME, password=password) self.send_plugins = get_sending_plugins(MODULE.process) self.password_reset_plugins = {k: v for k, v in self.send_plugins.items() if v.message_application() == 'password_reset'}
@property def usersmod(self): if not self._usersmod: univention.admin.modules.update() self._usersmod = univention.admin.modules.get('users/user') if not self._usersmod.initialized: lo, po = get_machine_connection() univention.admin.modules.init(lo, po, self._usersmod) return self._usersmod
[docs] @forward_to_master @sanitize( username=StringSanitizer(required=True, minimum=1), password=StringSanitizer(required=True, minimum=1)) @simple_response def get_service_specific_passwords(self, username, password): """ Get users (possible) service specific passwords. :return: list of dicts with users ssp """ if ucr.is_false('umc/self-service/service-specific-passwords/backend/enabled'): msg = _('Service specific passwords were disabled via the Univention Configuration Registry.') MODULE.error("get_service_specific_passwords(): %s", msg) raise UMC_Error(msg) dn, username = self.auth(username, password) ret = [] # we only have radius as a service specific password: setting # the backend to true means that the admin wants to allow # radius password. at some point we would need to know which # services should actually be managed ldap_connection, _ldap_position = getMachineConnection() radius_passwords = ldap_connection.get(dn, attr=['univentionRadiusPassword']).get('univentionRadiusPassword', []) ret.append({'type': 'radius', 'set': len(radius_passwords)}) return ret
[docs] @forward_to_master @sanitize( username=StringSanitizer(required=True, minimum=1), password=StringSanitizer(required=True, minimum=1), password_type=StringSanitizer(required=True, minimum=1)) @simple_response def set_service_specific_passwords(self, username, password, password_type): """ Set a new service specific password. :return: The password in cleartext """ if ucr.is_false('umc/self-service/service-specific-passwords/backend/enabled'): msg = _('Service specific passwords were disabled via the Univention Configuration Registry.') MODULE.error("get_service_specific_passwords(): %s", msg) raise UMC_Error(msg) dn, username = self.auth(username, password) MODULE.error("set_service_specific_passwords(): Setting %s password for %s", password_type, username) if password_type == 'radius': udm = UDMRest.http('https://%s/univention/udm/' % UDM_REST_SERVER, 'cn=admin', open('/etc/ldap.secret').read()) user_obj = udm.get('users/user').get(dn) service_specific_password = user_obj.generate_service_specific_password('radius') else: msg = _('Service specific passwords were disabled for "%s".') % password_type MODULE.error("get_service_specific_passwords(): %s", msg) raise UMC_Error(msg) return {'password': service_specific_password}
[docs] @forward_to_master @sanitize( username=StringSanitizer(required=True, minimum=1), password=StringSanitizer(required=True, minimum=1)) @simple_response def get_contact(self, username, password): """ Get users contact data. :return: list of dicts with users contact data """ if ucr.is_false('umc/self-service/protect-account/backend/enabled'): msg = _('The account protection was disabled via the Univention Configuration Registry.') MODULE.error("get_contact(): %s", msg) raise UMC_Error(msg) _dn, username = self.auth(username, password) if self.is_blacklisted(username, 'passwordreset'): # FIXME: should be 'protect-account' raise ServiceForbidden() user = self.get_udm_user(username=username) if not self.password_reset_plugins: raise ServiceForbidden() return [{ "id": p.send_method(), "label": p.send_method_label(), "value": user[p.udm_property], } for p in self.password_reset_plugins.values() if p.udm_property in user]
[docs] @forward_to_master @sanitize( username=StringSanitizer(required=DISALLOW_AUTHENTICATION, minimum=1), password=StringSanitizer(required=DISALLOW_AUTHENTICATION, minimum=1)) @simple_response def get_user_attributes(self, username=None, password=None): # MUST be supported until UCS 4.4-7 is out of maintenance dn, username = self.authenticate_user(username, password) if self.is_blacklisted(username, 'profiledata'): raise ServiceForbidden() user = self.get_udm_user_by_dn(dn) user.set_defaults = True user.set_default_values() properties = user.info.copy() widget_descriptions = [ dict(wd, value=properties.get(wd['id'])) for wd in self._get_user_attributes_descriptions() if user.has_property(wd['id']) ] # TODO: make layout configurable via ucr ? layout = [wd['id'] for wd in widget_descriptions] return { 'widget_descriptions': widget_descriptions, 'layout': layout, }
[docs] @forward_to_master_if_authentication_disabled @sanitize( username=StringSanitizer(required=DISALLOW_AUTHENTICATION, minimum=1), password=StringSanitizer(required=DISALLOW_AUTHENTICATION, minimum=1)) @simple_response def get_user_attributes_values(self, attributes, username=None, password=None): dn, username = self.authenticate_user(username, password) if self.is_blacklisted(username, 'profiledata'): raise ServiceForbidden() user_attributes = [attr.strip() for attr in ucr.get('self-service/udm_attributes', '').split(',')] user = self.get_udm_user_by_dn(dn) user.set_defaults = True user.set_default_values() properties = user.info.copy() return { prop: properties.get(prop) for prop in attributes if user.has_property(prop) and prop in user_attributes }
[docs] @forward_to_master @simple_response def get_user_attributes_descriptions(self): return self._get_user_attributes_descriptions()
def _get_user_attributes_descriptions(self): user_attributes = [attr.strip() for attr in ucr.get('self-service/udm_attributes', '').split(',')] read_only_attributes = [attr.strip() for attr in ucr.get('self-service/udm_attributes/read-only', '').split(',')] widget_descriptions = [] label_overwrites = { 'jpegPhoto': _('Your picture'), } for propname in user_attributes: if propname == 'password': continue prop = self.usersmod.property_descriptions.get(propname) if not prop: continue widget_description = { 'id': propname, 'label': label_overwrites.get(propname, prop.short_description), 'description': prop.long_description, 'syntax': prop.syntax.name, 'size': prop.size or prop.syntax.size, 'required': bool(prop.required), 'editable': bool(prop.may_change), 'readonly': not bool(prop.editable) or propname in read_only_attributes, 'multivalue': bool(prop.multivalue), } widget_description.update(prop.syntax.get_widget_options(prop)) if 'udm' in widget_description['type']: continue if 'dynamicValues' in widget_description: continue widget_descriptions.append(widget_description) return widget_descriptions
[docs] @forward_to_master @simple_response def get_registration_attributes(self): ucr.load() property_ids = ['PasswordRecoveryEmail', 'password'] for id_ in [attr.strip() for attr in ucr.get('umc/self-service/account-registration/udm_attributes', '').split(',') if attr.strip()]: if id_ not in property_ids: property_ids.append(id_) lo, po = get_machine_connection() users_mod = UDM_Module('users/user', True, lo, po) properties = {prop['id']: prop for prop in users_mod.properties(None)} not_existing = set(property_ids) - set(properties.keys()) properties = {k: v for (k, v) in properties.items() if 'dynamicValues' not in v and 'udm' not in v['type']} # filter out not supported props not_supported = set(property_ids) - set(properties.keys()) - not_existing if 'PasswordRecoveryEmail' in properties: properties['PasswordRecoveryEmail']['label'] = _('Recovery email address') properties['PasswordRecoveryEmail']['description'] = _('Enter an email address that you can access if you lose access to this account. We will use it to help you recover your account if you forget your password.') self._update_required_attr_of_props_for_registration(properties) properties = [properties[id_] for id_ in property_ids if id_ in properties] if not_existing: MODULE.warning("get_registration_attributes(): the following attributes defined by umc/self-service/account-registration/udm_attributes do not exist on users/user: %s", ", ".join(not_existing)) if not_supported: MODULE.warning("get_registration_attributes(): the following attributes defined by umc/self-service/account-registration/udm_attributes are not supported: %s", ", ".join(not_supported)) return { 'widget_descriptions': properties, 'layout': [prop['id'] for prop in properties], }
def _update_required_attr_of_props_for_registration(self, properties): for k in properties.keys(): if isinstance(properties[k], dict): properties[k]['required'] = False else: properties[k].required = False required_ids = set(['PasswordRecoveryEmail', 'password'] + [attr.strip() for attr in ucr.get('umc/self-service/account-registration/udm_attributes/required', '').split(',') if attr.strip()]) for id_ in required_ids: if id_ in properties: if isinstance(properties[id_], dict): properties[id_]['required'] = True else: properties[id_].required = True
[docs] @forward_to_master_if_authentication_disabled @sanitize( username=StringSanitizer(required=DISALLOW_AUTHENTICATION, minimum=1), password=StringSanitizer(required=DISALLOW_AUTHENTICATION, minimum=1)) @simple_response def validate_user_attributes(self, username, password, attributes): _dn, username = self.authenticate_user(username, password) if self.is_blacklisted(username, 'profiledata'): raise ServiceForbidden() return self._validate_user_attributes(attributes)
def _validate_user_attributes(self, attributes, map_properties_func=None): res = {} properties = self.usersmod.property_descriptions if map_properties_func: properties = properties.copy() map_properties_func(properties) for propname, value in attributes.items(): prop = properties.get(propname) if not prop: continue isValid = True message = '' if prop.multivalue and isinstance(value, tuple | list): isValid = [] message = [] for ival in value: _isValid = True _message = '' try: prop.syntax.parse(ival) except (udm_errors.valueError, udm_errors.valueInvalidSyntax) as e: _isValid = False _message = str(e) finally: isValid.append(_isValid) message.append(_message) else: try: prop.syntax.parse(value) except (udm_errors.valueError, udm_errors.valueInvalidSyntax) as e: isValid = False message = str(e) if prop.required and not value: isValid = False message = _('This value is required') res[propname] = { 'isValid': isValid, 'message': message, } return res
[docs] @forward_to_master_if_authentication_disabled @sanitize( attributes=DictSanitizer({}, required=True), username=StringSanitizer(required=DISALLOW_AUTHENTICATION, minimum=1), password=StringSanitizer(required=DISALLOW_AUTHENTICATION, minimum=1)) def set_user_attributes(self, request): attributes = request.options['attributes'] username = request.options.get('username') password = request.options.get('password') dn, username = self.authenticate_user(username, password) username = username or request.username if password: dn, username = self.auth(username, password) lo, po = get_user_connection(binddn=dn, bindpw=password) else: lo = request.get_user_ldap_connection(write=True) po = univention.admin.uldap.position(lo.base) if self.is_blacklisted(username, 'profiledata'): raise ServiceForbidden() user_attributes = [attr.strip() for attr in ucr.get('self-service/udm_attributes', '').split(',')] read_only_attributes = [attr.strip() for attr in ucr.get('self-service/udm_attributes/read-only', '').split(',')] for attr in attributes: if attr in read_only_attributes: MODULE.error('set_user_attributes(): attribute %s is read-only', attr) raise UMC_Error(_('The attribute %s is read-only.') % (attr,)) uploads = {k: v for k, v in attributes.items() if k in user_attributes} self._scan_uploads_and_execute(request, uploads, username, '_save_user_attributes', dn, lo, po, attributes, user_attributes)
def _save_user_attributes(self, request, dn, lo, po, attributes, user_attributes): """ Save the profile data after the malware scan. Runs on the IOLoop through :meth:`execute`. :param request: The UMC request. :param str dn: The DN of the user. :param lo: The LDAP connection of the user. :param po: The LDAP position. :param dict attributes: The attribute values that the user sent. :param list user_attributes: The attributes that the user may change. :raises UMC_Error: If UDM can't save the data. """ user = self.usersmod.object(None, lo, po, dn) user.open() for propname, value in attributes.items(): if propname in user_attributes and user.has_property(propname): user[propname] = value try: user.modify() except udm_errors.base as exc: MODULE.exception('set_user_attributes(): modifying the user failed:') raise UMC_Error(_('The attributes could not be saved: %s') % (UDM_Error(exc))) self.finished(request.id, _("Successfully changed your profile data.")) def _get_password_complexity_message(self): return ucr.get( 'umc/login/password-complexity-message/%s' % (self.locale.language,), ucr.get('umc/login/password-complexity-message/en', ''), )
[docs] @forward_to_master @sanitize(attributes=DictSanitizer({}, required=True)) def create_self_registered_account(self, request): attributes = request.options['attributes'] MODULE.info("create_self_registered_account(): attributes: %s", attributes) ucr.load() if ucr.is_false('umc/self-service/account-registration/backend/enabled', True): msg = _('The account registration was disabled via the Univention Configuration Registry.') MODULE.error("create_self_registered_account(): %s", msg) raise UMC_Error(msg) # filter out attributes that are not valid to set allowed_to_set = set(['PasswordRecoveryEmail', 'password'] + [attr.strip() for attr in ucr.get('umc/self-service/account-registration/udm_attributes', '').split(',') if attr.strip()]) attributes = {k: v for (k, v) in attributes.items() if k in allowed_to_set} check_upload_limit(self.usersmod.property_descriptions, attributes, malware_scan_settings()) # validate attributes res = self._validate_user_attributes(attributes, self._update_required_attr_of_props_for_registration) # check username taken if 'username' in attributes: try: UDM.machine().version(2).get('users/user').get_by_id(attributes['username']) except NoObject: pass else: res['username'] = { 'isValid': False, 'message': _('The username is already taken'), } invalid = {k: v for (k, v) in res.items() if not (all(v['isValid']) if isinstance(v['isValid'], list) else v['isValid'])} if invalid: self.finished(request.id, { 'success': False, 'failType': 'INVALID_ATTRIBUTES', 'data': invalid, }) return # check for missing required attributes from umc/self-service/account-registration/udm_attributes/required required_attrs = [attr.strip() for attr in ucr.get('umc/self-service/account-registration/udm_attributes/required', '').split(',') if attr.strip()] not_found = [attr for attr in required_attrs if attr not in attributes] if not_found: msg = _('The account could not be created:\nInformation provided is not sufficient. The following properties are missing:\n%s') % ('\n'.join(not_found),) MODULE.error("create_self_registered_account(): %s", msg) raise UMC_Error(msg) self._scan_uploads_and_execute(request, attributes, attributes.get('username'), '_create_self_registered_account', attributes)
def _scan_uploads_and_execute(self, request, attributes, username, method, *args): """ Scan the uploads in a worker thread and then run the command method on the IOLoop. The settings and the decoded files are prepared here on the IOLoop with the current property descriptions. The thread only gets that data, so that it doesn't touch shared state of the module. After a clean scan the method runs through :meth:`execute`, which does the regular error handling. Without an ICAP server or without uploads the method runs directly. With an ICAP server, a request with more files than allowed is rejected before any file is decoded or scanned, see :func:`check_upload_limit`. :param request: The UMC request. :param dict attributes: The attribute values that the user sent. :param str username: The name of the user, for the log messages. :param str method: Name of the request handler that saves the data and finishes the request. :param args: Further arguments for the request handler. """ settings = malware_scan_settings() check_upload_limit(self.usersmod.property_descriptions, attributes, settings) files = uploaded_files(self.usersmod.property_descriptions, attributes) if settings.url else [] if not files: self.execute(method, request, *args) return def scanned(thread, result): if isinstance(result, BaseException): self.thread_finished_callback(thread, result, request) elif not self._is_active(request): MODULE.info('Request %s was cancelled while the malware scan was running', request.id) else: self.execute(method, request, *args) SimpleThread('malware_scan', scan_files_for_malware, scanned).run(files, settings, username) def _create_self_registered_account(self, request, attributes): """ Create the account after the validation and the malware scan. Runs on the IOLoop through :meth:`execute`. :param request: The UMC request. :param dict attributes: The validated attributes of the new account. """ univention.admin.modules.update() lo, po = get_admin_connection() # get usertemplate template_dn = ucr.get('umc/self-service/account-registration/usertemplate', '') usertemplate = None if template_dn: usertemplate_mod = univention.admin.modules.get('settings/usertemplate') univention.admin.modules.init(lo, po, usertemplate_mod, None, True) try: usertemplate = usertemplate_mod.object(None, lo, None, template_dn) except udm_errors.noObject: msg = _('The user template "{template_dn}" set by the "umc/self-service/account-registration/usertemplate" UCR variable does not exist. A user account can not be created. Please contact your system administrator.').format(template_dn=template_dn) MODULE.error("create_self_registered_account(): %s", msg) raise UMC_Error(msg) # init user module with template usersmod = univention.admin.modules.get('users/user') univention.admin.modules.init(lo, po, usersmod, usertemplate, True) # get user container udm = UDM.machine().version(2) user_position = univention.admin.uldap.position(po.getBase()) container_dn = ucr.get('umc/self-service/account-registration/usercontainer', None) if container_dn: try: container = udm.obj_by_dn(container_dn) except NoObject: msg = _('The container "{container_dn}" set by the "umc/self-service/account-registration/usercontainer" UCR variable does not exist. A user account can not be created. Please contact your system administrator.').format(container_dn=container_dn) MODULE.error("create_self_registered_account(): %s", msg) raise UMC_Error(msg) else: user_position.setDn(container.dn) else: for dn in usersmod.object.get_default_containers(lo): try: container = udm.obj_by_dn(dn) except NoObject: pass else: user_position.setDn(container.dn) break # create user attributes['PasswordRecoveryEmailVerified'] = 'FALSE' attributes['RegisteredThroughSelfService'] = 'TRUE' new_user = usersmod.object(None, lo, user_position) new_user.open() for key, value in attributes.items(): if key in new_user and value: new_user[key] = value try: new_user.create() except univention.admin.uexceptions.base as exc: password_complexity_message = self._get_password_complexity_message() if isinstance(exc, udm_errors.pwToShort | udm_errors.pwQuality) else '' MODULE.error('create_self_registered_account(): could not create user: %s', exc) self.finished(request.id, { 'success': False, 'failType': 'CREATION_FAILED', 'data': (_('The account could not be created:\n%s\n%s') % (UDM_Error(exc), password_complexity_message)).rstrip(), }) return finally: # TODO: cleanup # reinit user module without template. # This has to be done since the modules are singletons? univention.admin.modules.update() self._usersmod = None # univention.admin.modules.init(lo, po, usersmod, None, True) try: # in all SS cases we need more than the previously default fields user_info = self._extract_user_properties(new_user) self.send_message( new_user['username'], 'verify_email', new_user['PasswordRecoveryEmail'], user_info, ) except Exception: MODULE.exception('could not send message') verify_token_successfully_send = False else: verify_token_successfully_send = True self.finished(request.id, { 'success': True, 'verifyTokenSuccessfullySend': verify_token_successfully_send, 'data': { 'username': new_user['username'], 'email': new_user['PasswordRecoveryEmail'], }, }) def _extract_user_properties(self, user_obj): message_fields = [ 'username', 'title', 'initials', 'displayName', 'organisation', 'employeeNumber', 'firstname', 'lastname', 'mailPrimaryAddress', ] info_out = {field: user_obj.info.get(field, '') for field in message_fields} return info_out
[docs] @forward_to_master @prevent_denial_of_service @sanitize( username=StringSanitizer(required=True)) @simple_response def send_verification_token(self, username): MODULE.info("send_verification_token(): username: %s", username) ucr.load() if ucr.is_false('umc/self-service/account-verification/backend/enabled', True): msg = _('The account verification was disabled via the Univention Configuration Registry.') MODULE.error("send_verification_token(): %s", msg) raise UMC_Error(msg) invalid_information = { 'success': False, 'failType': 'INVALID_INFORMATION', } users_mod = UDM.machine().version(2).get('users/user') try: user = users_mod.get_by_id(username) except NoObject: return invalid_information try: email = user.props.PasswordRecoveryEmail except AttributeError: return invalid_information else: if not email: return invalid_information user_info = self._extract_user_properties(user._orig_udm_object) self.send_message(username, 'verify_email', email, user_info) return { 'success': True, 'data': { 'username': username, }, }
[docs] @forward_to_master @prevent_denial_of_service @sanitize( username=StringSanitizer(required=True), password=StringSanitizer(required=True), email=StringSanitizer(required=False), mobile=StringSanitizer(required=False)) @simple_response def set_contact(self, username, password, email=None, mobile=None): if ucr.is_false('umc/self-service/protect-account/backend/enabled'): msg = _('The account protection was disabled via the Univention Configuration Registry.') MODULE.error("set_contact(): %s", msg) raise UMC_Error(msg) MODULE.info("set_contact(): username: %s password: ***** email: %s mobile: %s", username, email, mobile) dn, username = self.auth(username, password) if self.is_blacklisted(username, 'passwordreset'): raise ServiceForbidden() try: return self.set_contact_data(dn, email, mobile) except Exception: raise UMC_Error(_('Changing contact data failed.'), status=500)
[docs] @forward_to_master @prevent_denial_of_service @sanitize( username=StringSanitizer(required=True), method=StringSanitizer(required=True)) @simple_response def send_token(self, username, method): if ucr.is_false('umc/self-service/passwordreset/backend/enabled'): msg = _('The password reset was disabled via the Univention Configuration Registry.') MODULE.error("send_token(): %s", msg) raise UMC_Error(msg) MODULE.info("send_token(): username: '%s' method: '%s'.", username, method) try: plugin = self.password_reset_plugins[method] except KeyError: MODULE.error("send_token() method '%s' not in %s.", method, self.password_reset_plugins.keys()) raise UMC_Error(_("Unknown recovery method '{}'.").format(method)) if self.is_blacklisted(username, 'passwordreset'): raise TokenSendMessage() # check if the user has the required attribute set user = self.get_udm_user(username=username) username = user["username"] if len(user[plugin.udm_property]) > 0: # found contact info user_info = self._extract_user_properties(user) self.send_message(username, method, user[plugin.udm_property], user_info) raise TokenSendMessage()
[docs] @forward_to_master @prevent_denial_of_service @sanitize( token=StringSanitizer(required=True), username=StringSanitizer(required=True), method=StringSanitizer(required=True), ) @simple_response def verify_contact(self, token, username, method): MODULE.info("verify_contact(): token: %s username: %s method: %s", token, username, method) ucr.load() if ucr.is_false('umc/self-service/account-verification/backend/enabled', True): msg = _('The account verification was disabled via the Univention Configuration Registry.') MODULE.error("verify_contact(): %s", msg) raise UMC_Error(msg) users_mod = UDM.admin().version(1).get('users/user') try: user = users_mod.get_by_id(username) except NoObject: return { 'success': False, 'failType': 'INVALID_INFORMATION', } next_steps = ucr.get('umc/self-service/account-verification/next-steps/%s' % self.locale.language, '') if not next_steps: next_steps = ucr.get('umc/self-service/account-verification/next-steps', '') plugin = self._get_send_plugin(method) if getattr(user.props, plugin.udm_property) == 'TRUE': # cleanup. map property to actual boolean? return { 'success': True, 'successType': 'ALREADY_VERIFIED', 'data': { 'username': username, 'nextSteps': next_steps, }, } self._check_token(username, token, token_application=plugin.message_application()) setattr(user.props, plugin.udm_property, 'TRUE') user.save() self.db.delete_tokens(token=token, username=username) return { 'success': True, 'successType': 'VERIFIED', 'data': { 'username': username, 'nextSteps': next_steps, }, }
[docs] @forward_to_master @prevent_denial_of_service @sanitize( username=StringSanitizer(required=True), password=StringSanitizer(required=True), ) @simple_response def deregister_account(self, username, password): MODULE.info("deregister_account(): username: %s password: *****", username) ucr.load() if ucr.is_false('umc/self-service/account-deregistration/enabled', True): msg = _('The account deregistration was disabled via the Univention Configuration Registry.') MODULE.error("deregister_account(): %s", msg) raise UMC_Error(msg) _dn, username = self.auth(username, password) if self.is_blacklisted(username, 'account-deregistration'): raise ServiceForbidden() try: return self._deregister_account(username) except Exception: raise UMC_Error(_('Account could not be deleted'), status=500)
def _deregister_account(self, username): try: user = UDM.admin().version(2).get('users/user').get_by_id(username) user.props.DeregisteredThroughSelfService = 'TRUE' user.props.DeregistrationTimestamp = dt.datetime.strftime(dt.datetime.utcnow(), DEREGISTRATION_TIMESTAMP_FORMATTING) user.props.disabled = True user.save() try: self._notify_about_account_deregistration(user.props.username, user.props.PasswordRecoveryEmail) except Exception: MODULE.exception("_deregister_account(): sending of email failed") return except Exception: MODULE.exception("_deregister_account()") raise def _notify_about_account_deregistration(self, username, mail): if not mail: return ucr.load() path_ucr = ucr.get("umc/self-service/account-deregistration/email/text_file") if path_ucr and os.path.exists(path_ucr): path = path_ucr else: path = "/usr/share/univention-self-service/email_bodies/deregistration_notification_email_body.txt" with open(path) as fp: txt = fp.read() txt = txt.format(username=username) msg = MIMENonMultipart('text', 'plain', charset='utf-8') msg["Subject"] = ucr.get("umc/self-service/account-deregistration/email/subject", "Account deletion") msg["Date"] = formatdate(localtime=True) msg["From"] = ucr.get("umc/self-service/account-deregistration/email/sender_address", "Password Reset Service <noreply@{}>".format(".".join([ucr["hostname"], ucr["domainname"]]))) msg["To"] = mail cs = email.charset.Charset("utf-8") cs.body_encoding = email.charset.QP msg.set_payload(txt, charset=cs) smtp = smtplib.SMTP(ucr.get("umc/self-service/account-deregistration/email/server", "localhost")) smtp.sendmail(msg["From"], msg["To"], msg.as_string()) smtp.quit()
[docs] @forward_to_master @prevent_denial_of_service @sanitize( token=StringSanitizer(required=True), username=StringSanitizer(required=True), password=StringSanitizer(required=True)) # new_password(!) @simple_response def set_password(self, token, username, password): MODULE.info("set_password(): username: '%s'.", username) username = self.email2username(username) token_from_db = self._check_token(username, token) # token is correct and valid MODULE.info("Receive valid token for '%s'.", username) if self.is_blacklisted(username, 'passwordreset'): # this should not happen MODULE.error("Found token in DB for blacklisted user '%s'.", username) self.db.delete_tokens(token=token, username=username) raise ServiceForbidden() # TokenNotFound() ? plugin = self._get_send_plugin(token_from_db['method']) email_verified = plugin.password_reset_verified_recovery_email() ret = self.udm_set_password(username, password, email_verified=email_verified) self.db.delete_tokens(token=token, username=username) if ret: raise UMC_Error(_("Successfully changed your password."), status=200) raise UMC_Error(_('Failed to change password.'), status=500)
def _check_token(self, username, token, token_application='password_reset'): try: token_from_db = self.db.get_one(token=token, username=username) except MultipleTokensInDB as e: # this should not happen, delete all tokens, raise Exception # regardless of correctness of token MODULE.error("set_password(): %s", e) self.db.delete_tokens(token=token, username=username) raise TokenNotFound() if not token_from_db: # no token in DB MODULE.info("Token not found in DB for user '%s'.", username) raise TokenNotFound() if (dt.datetime.utcnow() - token_from_db["timestamp"]).total_seconds() >= self.token_validity_period: # token is correct but expired MODULE.info("Receive correct but expired token for '%s'.", username) self.db.delete_tokens(token=token, username=username) raise TokenNotFound() if self._get_send_plugin(token_from_db['method']).message_application() != token_application: # token is correct but should not be used for this application MODULE.info("Receive correct token for '%s' but it should be used for another application.", username) self.db.delete_tokens(token=token, username=username) raise TokenNotFound() return token_from_db
[docs] @forward_to_master @simple_response def get_reset_methods(self) -> list[dict[str, Any]]: if ucr.is_false('umc/self-service/passwordreset/backend/enabled'): msg = _('The password reset was disabled via the Univention Configuration Registry.') MODULE.error("get_reset_methods(): %s", msg) raise UMC_Error(msg) reset_methods = [{ "id": p.send_method(), "label": p.send_method_label(), } for p in self.password_reset_plugins.values()] if not reset_methods: raise NoMethodsAvailable() return reset_methods
[docs] @staticmethod def create_token(length): # remove easily confusable characters chars = ''.join(set(string.ascii_letters) | set(string.digits) - {"0", "O", "1", "I", "l"}) rand = random.SystemRandom() return ''.join(rand.choice(chars) for _ in range(length))
[docs] def send_message(self, username, method, address, user_properties): plugin = self._get_send_plugin(method) try: token_from_db = self.db.get_one(username=username) except MultipleTokensInDB as e: # this should not happen, delete all tokens MODULE.error("send_token(): %s", e) self.db.delete_tokens(username=username) token_from_db = None token = self.create_token(plugin.token_length) if token_from_db: # replace with fresh token MODULE.info("send_token(): Updating token for user '%s'...", username) self.db.update_token(username, method, token) else: # store a new token MODULE.info("send_token(): Adding new token for user '%s'...", username) self.db.insert_token(username, method, token) try: self._call_send_msg_plugin(username, method, address, token, user_properties) except Exception: MODULE.error("send_token(): Error sending token with via '%s' to '%s'.", method, username) self.db.delete_tokens(username=username) raise return True
def _get_send_plugin(self, method): try: plugin = self.send_plugins[method] if not plugin.is_enabled: raise KeyError except KeyError: raise UMC_Error("Unknown send message method", status=500) return plugin def _call_send_msg_plugin(self, username, method, address, token, user_properties): MODULE.info("send_message(): username: %s method: %s address: %s", username, method, address) plugin = self._get_send_plugin(method) plugin.set_data({ "username": username, "address": address, "token": token, "user_properties": user_properties, }) MODULE.info("send_message(): Running plugin of class %s...", plugin.__class__.__name__) try: plugin.send() except Exception as exc: MODULE.exception('Unknown error') raise UMC_Error(_("Error sending token: {}").format(exc), status=500) return True
[docs] @staticmethod @machine_connection def auth(username, password, ldap_connection=None, ldap_position=None): filter_s = filter_format("(|(uid=%s)(mailPrimaryAddress=%s))", (username, username)) users = ldap_connection.search(filter=filter_s) try: binddn, userdict = users[0] get_user_connection(binddn=binddn, bindpw=password) except (udm_errors.authFail, IndexError): raise ServiceForbidden() return binddn, userdict["uid"][0].decode('utf-8')
[docs] def authenticate_user(self, username=None, password=None): """Check if the user is already authenticated (via UMC/SAML login) or use the credentials provided via the form.""" if username and password: # credentials provided, use them dn, username = self.auth(username, password) return (dn, username) elif self.user_dn and self.username and not DISALLOW_AUTHENTICATION: # logged in via SAML/UMC return (self.user_dn, self.username) else: # malformed request, cannot really happen raise UMC_Error('Please provide username and password.')
[docs] def set_contact_data(self, dn, email, mobile): try: user = self.get_udm_user_by_dn(userdn=dn, admin=True) old_email = user['PasswordRecoveryEmail'] if email is not None and email.lower() != old_email.lower(): try: user["PasswordRecoveryEmail"] = email except udm_errors.valueInvalidSyntax as err: raise UMC_Error(err) else: user['PasswordRecoveryEmailVerified'] = 'FALSE' if mobile is not None and mobile.lower() != user["PasswordRecoveryMobile"].lower(): user["PasswordRecoveryMobile"] = mobile user.modify() verification_email_send = False if user['RegisteredThroughSelfService'] == 'TRUE': if old_email is not None and old_email.lower() != email.lower(): self._notify_about_email_change(user['username'], old_email, email) if email is not None and email.lower() != old_email.lower(): user_info = self._extract_user_properties(user) self.send_message(user['username'], 'verify_email', email, user_info) verification_email_send = True return { 'verificationEmailSend': verification_email_send, 'email': email, } except Exception: MODULE.exception("set_contact_data():") raise
def _notify_about_email_change(self, username, old_email, new_email): if not old_email: return new_email = new_email or '' ucr.load() path_ucr = ucr.get("umc/self-service/email-change-notification/email/text_file") if path_ucr and os.path.exists(path_ucr): path = path_ucr else: path = "/usr/share/univention-self-service/email_bodies/email_change_notification_email_body.txt" with open(path) as fp: txt = fp.read() txt = txt.format(username=username, old_email=old_email, new_email=new_email) msg = MIMENonMultipart('text', 'plain', charset='utf-8') msg["Subject"] = ucr.get("umc/self-service/email-change-notification/email/subject", "Account recovery email changed") msg["Date"] = formatdate(localtime=True) msg["From"] = ucr.get("umc/self-service/passwordreset/email/sender_address", "Password Reset Service <noreply@{}>".format(".".join([ucr["hostname"], ucr["domainname"]]))) msg["To"] = old_email cs = email.charset.Charset("utf-8") cs.body_encoding = email.charset.QP msg.set_payload(txt, charset=cs) smtp = smtplib.SMTP(ucr.get("umc/self-service/passwordreset/email/server", "localhost")) smtp.sendmail(msg["From"], msg["To"], msg.as_string()) smtp.quit()
[docs] def admember_set_password(self, username, password): ldb_url = ucr.get('connector/ad/ldap/host') ldb_url = 'ldaps://%s' % (ldb_url,) if ucr.is_true('connector/ad/ldap/ldaps') else 'ldap://%s' % (ldb_url,) try: reset_username = dict(ucr)['ad/reset/username'] with open(dict(ucr)['ad/reset/password']) as fd: reset_password = fd.readline().strip() except (OSError, KeyError): raise UMC_Error(_('The configuration of the password reset service is not complete. The UCR variables "ad/reset/username" and "ad/reset/password" need to be set properly. Please inform an administrator.'), status=500) process = Popen(['samba-tool', 'user', 'setpassword', '--username', reset_username, '--password', reset_password, '--filter', filter_format('samaccountname=%s', (username,)), '--newpassword', password, '-H', ldb_url], stdout=PIPE, stderr=STDOUT) stdouterr = process.communicate()[0].decode('utf-8', 'replace') if stdouterr: MODULE.process('samba-tool user setpassword: %s', stdouterr) if process.returncode: MODULE.error("admember_set_password(): failed to set password. Return code: %s", process.returncode) return False return True
[docs] def udm_set_password(self, username, password, email_verified): user = self.get_udm_user(username=username, admin=True) if ucr.is_true('ad/member') and 'synced' in user.get('objectFlag', []): success = self.admember_set_password(username, password) else: user["password"] = password user["pwdChangeNextLogin"] = 0 success = True if email_verified: user["PasswordRecoveryEmailVerified"] = 'TRUE' try: user.modify() except (udm_errors.pwToShort, udm_errors.pwQuality) as exc: password_complexity_message = ucr.get('umc/login/password-complexity-message/%s' % (self.locale.language,), ucr.get('umc/login/password-complexity-message/en', '')) raise UMC_Error(("%s %s" % (exc, password_complexity_message)).rstrip()) except udm_errors.pwalreadyused as exc: raise UMC_Error(exc.message) except Exception: MODULE.exception("udm_set_password(): failed to set password") raise else: return success
# TODO: decoratorize
[docs] @machine_connection def is_blacklisted(self, username, feature, ldap_connection=None, ldap_position=None): def listize(li): return [x.strip().lower() for x in li.split(",") if x.strip()] bl_users = listize(ucr.get(f"umc/self-service/{feature}/blacklist/users", "")) bl_groups = listize(ucr.get(f"umc/self-service/{feature}/blacklist/groups", "")) wh_users = listize(ucr.get(f"umc/self-service/{feature}/whitelist/users", "")) wh_groups = listize(ucr.get(f"umc/self-service/{feature}/whitelist/groups", "")) username = self.email2username(username) # user blacklist if username.lower() in bl_users: MODULE.info("is_blacklisted(username: %s, feature: %s): match in blacklisted users", username, feature) return True # get groups try: filter_s = filter_format("(|(uid=%s)(mailPrimaryAddress=%s))", (username, username)) userdn = ldap_connection.searchDn(filter=filter_s)[0] groups_dns = self.get_groups(userdn) for group_dn in list(groups_dns): groups_dns.extend(self.get_nested_groups(group_dn)) groups_dns = list(set(groups_dns)) gr_names = [x.lower() for x in self.dns_to_groupname(groups_dns)] except IndexError: # no user or no group found return True # group blacklist if any(gr in bl_groups for gr in gr_names): MODULE.info("is_blacklisted(username: %s, feature: %s): match in blacklisted groups", username, feature) return True # if not on blacklist, check whitelists # user whitelist if username.lower() in wh_users: MODULE.info("is_blacklisted(username: %s, feature: %s): match in whitelisted users", username, feature) return False # group whitelist if any(gr in wh_groups for gr in gr_names): MODULE.info("is_blacklisted(username: %s, feature: %s): match in whitelisted groups", username, feature) return False # not on either black or white list -> not allowed if whitelist exists, else OK MODULE.info("is_blacklisted(username: %s, feature: %s): neither black nor white listed", username, feature) return bool(wh_users or wh_groups)
[docs] def get_groups(self, userdn): user = self.get_udm_user_by_dn(userdn=userdn) groups = user["groups"] prim_group = user["primaryGroup"] if prim_group not in groups: groups.append(prim_group) return groups
[docs] def get_nested_groups(self, groupdn): group = self.get_udm_group(groupdn) res = group["memberOf"] or [] for ng in list(res): res.extend(self.get_nested_groups(ng)) return res
[docs] def dns_to_groupname(self, dns): names = [] for groupdn in dns: group = self.get_udm_group(groupdn) names.append(group["name"]) return names
[docs] def get_udm_user_by_dn(self, userdn, admin=False): if admin: lo, po = get_admin_connection() else: lo, po = get_machine_connection() user = self.usersmod.object(None, lo, po, userdn) user.open() return user
[docs] def get_udm_user(self, username, admin=False): filter_s = filter_format('(|(uid=%s)(mailPrimaryAddress=%s))', (username, username)) lo, _po = get_machine_connection() dn = lo.searchDn(filter=filter_s)[0] return self.get_udm_user_by_dn(dn, admin=admin)
[docs] @machine_connection def get_udm_group(self, groupdn, ldap_connection=None, ldap_position=None): # reuse module for recursive lookups by get_nested_groups() if not self.groupmod: univention.admin.modules.update() self.groupmod = univention.admin.modules.get("groups/group") univention.admin.modules.init(ldap_connection, ldap_position, self.groupmod) group = self.groupmod.object(None, ldap_connection, ldap_position, groupdn) group.open() return group
[docs] @machine_connection # TODO: overwrite StringSanitizer and do it there def email2username(self, email, ldap_connection=None, ldap_position=None): if "@" not in email: return email # cache email->username in memcache username = self.memcache.get(f"e2u:{email}") if not username: mailf = filter_format("(mailPrimaryAddress=%s)", (email,)) users = ldap_connection.search(filter=mailf) try: _, userdict = users[0] except IndexError: return email username = userdict["uid"][0].decode('utf-8') self.memcache.set(f"e2u:{email}", username, 300) return username