web2ldap  1.7.7
About: web2ldap is a full-featured web-based LDAPv3 client.
  Fossies Dox: web2ldap-1.7.7.tar.gz  ("unofficial" and yet experimental doxygen-generated source code documentation)  

handler.py
Go to the documentation of this file.
1# -*- coding: ascii -*-
2"""
3web2ldap.app.handler: base handler
4
5web2ldap - a web-based LDAP Client,
6see https://www.web2ldap.de for details
7
8(C) 1998-2022 by Michael Stroeder <michael@stroeder.com>
9
10This software is distributed under the terms of the
11Apache License Version 2.0 (Apache-2.0)
12https://www.apache.org/licenses/LICENSE-2.0
13"""
14
15import sys
16import inspect
17import socket
18import time
19import urllib.parse
20import logging
21from ipaddress import ip_address, ip_network
22from hmac import compare_digest
23
24import ldap0
25from ldap0.ldapurl import LDAPUrl, is_ldapurl
26from ldap0.dn import DNObj
27from ldap0.err import PasswordPolicyException, PasswordPolicyExpirationWarning
28from ldap0.schema.models import ObjectClass
29
30import web2ldapcnf
32
33from . import ErrorExit
34from ..ldaputil import AD_LDAP49_ERROR_CODES, AD_LDAP49_ERROR_PREFIX
35from ..web.forms import FormException
36from ..web.session import SessionException, MaxSessionPerIPExceeded, MaxSessionCountExceeded
37from ..web.helper import get_remote_ip
38from ..ldaputil.extldapurl import ExtendedLDAPUrl
39from ..ldapsession import (
40 LDAP_DEFAULT_TIMEOUT,
41 LDAPSession,
42 START_TLS_REQUIRED,
43 START_TLS_NO,
44 UsernameNotFound,
45 InvalidSimpleBindDN,
46 UsernameNotUnique,
47)
48from ..utctime import ts2repr
49from ..log import LogHelper, logger, log_exception
50from ..msbase import GrabKeys
51# Import the application modules
52from .gui import (
53 footer,
54 header,
55 read_template,
56 top_section,
57)
58from .cnf import LDAP_DEF, LDAP_URI_LIST_CHECK_DICT
59from . import passwd
60from .entry import DisplayEntry
61from .gui import exception_message, DNS_AVAIL
62from .form import Web2LDAPForm
63from .session import (
64 InvalidSessionInstance,
65 WrongSessionCookie,
66 session_store,
67)
68from .schema.syntaxes import syntax_registry
69from .stats import COMMAND_COUNT
70# action functions
71from .connect import w2l_connect
72from .locate import w2l_locate
73from .monitor import w2l_monitor
74from .urlredirect import w2l_urlredirect
75from .searchform import w2l_searchform
76from .search import w2l_search
77from .add import w2l_add
78from .modify import w2l_modify
79from .dds import w2l_dds
80from .bulkmod import w2l_bulkmod
81from .delete import w2l_delete
82from .dit import w2l_dit
83from .rename import w2l_rename
84from .passwd import w2l_passwd
85from .read import w2l_read
86from .conninfo import w2l_conninfo
87from .params import w2l_params
88from .login import w2l_login
89from .groupadm import w2l_groupadm
90from .schema.viewer import w2l_schema_viewer
91from .metrics import w2l_metrics, METRICS_AVAIL
92from .srvrr import w2l_chasesrvrecord
93from .referral import w2l_chasereferral
94from .schema.syntaxes import Timespan
95
96if DNS_AVAIL:
97 from ..ldaputil.dns import dc_dn_lookup
98
99SCOPE2COMMAND = {
100 None:'search',
101 ldap0.SCOPE_BASE:'read',
102 ldap0.SCOPE_ONELEVEL:'search',
103 ldap0.SCOPE_SUBTREE:'search',
104 ldap0.SCOPE_SUBORDINATE:'search',
105}
106
107CONNTYPE2URLSCHEME = {
108 0: 'ldap',
109 1: 'ldap',
110 2: 'ldaps',
111 3: 'ldapi',
112}
113
114FORM_CLASS = {}
115logger.debug('Registering Form classes')
116for _, cls in inspect.getmembers(sys.modules['web2ldap.app.form'], inspect.isclass):
117 if issubclass(cls, Web2LDAPForm) and cls.command is not None:
118 logger.debug('Register class %s for command %r', cls.__name__, cls.command)
119 FORM_CLASS[cls.command] = cls
120
121SIMPLE_MSG_HTML = """
122<html>
123 <head>
124 <title>Note</title>
125 </head>
126 <body>
127 {message}
128 </body>
129</html>
130"""
131
132COMMAND_FUNCTION = {
133 '': w2l_connect,
134 'disconnect': None,
135 'locate': w2l_locate,
136 'monitor': w2l_monitor,
137 'urlredirect': w2l_urlredirect,
138 'searchform': w2l_searchform,
139 'search': w2l_search,
140 'add': w2l_add,
141 'modify': w2l_modify,
142 'dds': w2l_dds,
143 'bulkmod': w2l_bulkmod,
144 'delete': w2l_delete,
145 'dit': w2l_dit,
146 'rename': w2l_rename,
147 'passwd': w2l_passwd,
148 'read': w2l_read,
149 'conninfo': w2l_conninfo,
150 'params': w2l_params,
151 'login': w2l_login,
152 'groupadm': w2l_groupadm,
153 'oid': w2l_schema_viewer,
154}
155
156if METRICS_AVAIL:
157 COMMAND_FUNCTION['metrics'] = w2l_metrics
158
159syntax_registry.check()
160
161
163 """
164 Class implements web application entry point
165 and dispatches requests to use-case functions w2l_*()
166 """
167
168 def __init__(self, env, outf):
169 self.current_access_time = time.time()
170 self.inf = env['wsgi.input']
171 self.outf = outf
172 self.env = env
173 self.env.update(web2ldapcnf.httpenv_override)
174 self.script_name = self.env['SCRIPT_NAME']
175 self.command, self.sid = self.path_info(env)
176 self.form = None
177 self.ls = None
178 # class attributes later set by dn property method
179 self.dn_obj = None
180 self.query_string = env.get('QUERY_STRING', '')
181 self.ldap_url = None
182 self.schema = None
183 self.cfg_key = None
185 # initialize some more if query string is an LDAP URL
186 if is_ldapurl(self.query_string):
188 if not self.command:
189 self.command = SCOPE2COMMAND[self.ldap_url.scope]
190 # end of __init__()
191
192 @property
193 def dn(self):
194 """
195 get current DN
196 """
197 return str(self.dn_obj)
198
199 @dn.setter
200 def dn(self, dn):
201 """
202 set current DN and related class attributes
203 """
204 assert ldap0.dn.is_dn(dn), ValueError(
205 'Expected LDAP DN as dn, was %r' % (dn)
206 )
207 self.dn_obj = DNObj.from_str(dn)
208 if self.ls and self.ls.uri:
209 self.dn_obj.charset = self.ls.charset
210 self.schema = self.ls.get_sub_schema(
211 self.dndndn,
212 self.cfg_param('_schema', None),
213 self.cfg_param('supplement_schema', None),
214 self.cfg_param('schema_strictcheck', True),
215 )
216
217 @property
218 def naming_context(self):
219 if self.ls and self.ls.uri:
220 res = self.ls.get_search_root(self.dndndn)
221 else:
222 res = DNObj((()))
223 return res
224
225 @property
226 def audit_context(self):
227 if self.ls and self.ls.uri:
228 res = self.ls.get_audit_context(self.naming_context)
229 else:
230 res = None
231 return res
232
233 @property
234 def parent_dn(self):
235 """
236 get parent DN of current DN
237 """
238 return str(self.dn_obj.parent())
239
240 @property
241 def ldap_dn(self):
242 """
243 get LDAP encoding (UTF-8) of current DN
244 """
245 return bytes(self.dn_obj)
246
247 def cfg_param(self, param_key, default):
248 if self.ls and self.ls.uri:
249 cfg_url = self.ls.uri
250 else:
251 cfg_url = 'ldap://'
252 return LDAP_DEF.get_param(
253 cfg_url,
254 self.naming_context or '',
255 param_key,
256 default,
257 )
258
259 @property
260 def binddn_mapping(self):
261 """
262 get parameter 'binddn_mapping' from cascaded configuration
263 """
264 return self.cfg_param('binddn_mapping', 'ldap:///_??sub?(uid={user})')
265
266 def check_access(self, command):
267 """
268 simple access control based on client IP address
269 """
270 remote_addr = ip_address(self.env[web2ldapcnf.httpenv_remote_addr].split(',')[-1].strip())
271 access_allowed = web2ldapcnf.access_allowed.get(
272 command,
273 web2ldapcnf.access_allowed['_']
274 )
275 for net in access_allowed:
276 if remote_addr in ip_network(net, strict=False):
277 return True
278 return False
279
281 self,
282 command,
283 anchor_text,
284 form_parameters,
285 target=None,
286 title=None,
287 anchor_id=None,
288 rel=None,
289 ):
290 """
291 Build the HTML text of a anchor with form parameters
292 """
293 assert isinstance(command, str), \
294 TypeError('command must be str, but was %r', command)
295 assert isinstance(anchor_text, str), \
296 TypeError('anchor_text must be str, but was %r', anchor_text)
297 assert anchor_id is None or isinstance(anchor_id, str), \
298 TypeError('anchor_id must be None or str, but was %r', anchor_id)
299 assert target is None or isinstance(target, str), \
300 TypeError('target must be None or str, but was %r', target)
301 assert title is None or isinstance(title, str), \
302 TypeError('title must be None or str, but was %r', title)
303 target_attr = ''
304 if target:
305 target_attr = ' target="%s"' % (target)
306 title_attr = ''
307 if title:
308 title_attr = ' title="%s"' % (self.form.s2d(title).replace(' ', '&nbsp;'))
309 if anchor_id:
310 anchor_id = '#%s' % (self.form.s2d(anchor_id))
311 rel_attr = ''
312 if rel:
313 rel_attr = ' rel="%s"' % (rel)
314 res = '<a class="CL"%s%s%s href="%s?%s%s">%s</a>' % (
315 target_attr,
316 rel_attr,
317 title_attr,
318 self.form.action_url(command, self.sid),
319 '&amp;'.join([
320 '%s=%s' % (param_name, urllib.parse.quote(param_value))
321 for param_name, param_value in form_parameters
322 ]),
323 anchor_id or '',
324 anchor_text,
325 )
326 assert isinstance(res, str), TypeError('res must be str, was %r', res)
327 return res
328
329 def ldap_url_anchor(self, data):
330 if isinstance(data, LDAPUrl):
331 ldap_url = data
332 else:
333 ldap_url = LDAPUrl(ldapUrl=data)
334 command_func = {True:'read', False:'search'}[ldap_url.scope == ldap0.SCOPE_BASE]
335 if ldap_url.hostport:
336 command_text = 'Connect'
337 return self.anchor(
338 command_func,
339 'Connect and %s' % (command_func),
340 (('ldapurl', str(ldap_url)),)
341 )
342 command_text = {True:'Read', False:'Search'}[ldap_url.scope == ldap0.SCOPE_BASE]
343 return self.anchor(
344 command_func, command_text,
345 [
346 ('dn', ldap_url.dn),
347 ('filterstr', (ldap_url.filterstr or '(objectClass=*)')),
348 ('scope', str(ldap_url.scope or ldap0.SCOPE_SUBTREE)),
349 ],
350 )
351 # end of ldap_url_anchor()
352
354 self,
355 command,
356 method,
357 target=None,
358 enctype='application/x-www-form-urlencoded',
359 ):
360 """
361 convenience wrapper for Web2LDAPForm.begin_form()
362 which sets non-zero sid
363 """
364 return self.form.begin_form(
365 command,
366 self.sid,
367 method,
368 target=target,
369 enctype=enctype,
370 )
371
373 self,
374 command,
375 submitstr,
376 method,
377 form_parameters,
378 extrastr='',
379 target=None
380 ):
381 """
382 Build the HTML text of a submit form
383 """
384 form_str = [self.begin_form(command, method, target)]
385 for param_name, param_value in form_parameters:
386 form_str.append(self.form.hidden_field_html(param_name, param_value, ''))
387 form_str.append(
388 '<p>\n<input type="submit" value="%s">\n%s\n</p>\n</form>' % (
389 submitstr,
390 extrastr,
391 )
392 )
393 return '\n'.join(form_str)
394
395 def dispatch(self):
396 """
397 Execute function for self.command
398 """
399 assert isinstance(self.dndndn, str), \
400 TypeError(
401 "Class attribute %s.dn must be str, was %r" % (
402 self.__class__.__name__,
403 self.dndndn,
404 )
405 )
406 assert isinstance(self.ldap_url, ExtendedLDAPUrl), \
407 TypeError(
408 "Class attribute %s.ldap_url must be LDAPUrl instance, was %r" % (
409 self.__class__.__name__,
410 self.ldap_url,
411 )
412 )
413 self.log(logging.DEBUG, '%s.ldap_url is %s', self.__class__.__name__, self.ldap_url)
414 self.log(
415 logging.DEBUG,
416 'Dispatch command %r to function %s.%s()',
417 self.command,
418 COMMAND_FUNCTION[self.command].__module__,
419 COMMAND_FUNCTION[self.command].__name__,
420 )
421 COMMAND_FUNCTION[self.command](self)
422 # end of dispatch()
423
424 def path_info(self, env):
425 """
426 Extract the command and sid from PATH_INFO env var
427 """
428 path_info = env.get('PATH_INFO', '/')[1:]
429 self.log(logging.DEBUG, 'splitting path_info %r', path_info)
430 if not path_info:
431 sid, cmd = '', ''
432 else:
433 try:
434 sid, cmd = path_info.split('/', 1)
435 except ValueError:
436 sid, cmd = '', path_info
437 self.log(logging.DEBUG, 'split path_info to (%r, %r)', sid, cmd)
438 return cmd, sid
439 # path_info()
440
441 def display_dn(self, dn, links=False):
442 """Display a DN as LDAP URL with or without button"""
443 assert isinstance(dn, str), TypeError("Argument 'dn' must be str, was %r" % (dn,))
444 dn_str = self.form.s2d(dn or '- World -')
445 if links:
446 command_buttons = [
447 dn_str,
448 self.anchor('read', 'Read', [('dn', dn)])
449 ]
450 return web2ldapcnf.command_link_separator.join(command_buttons)
451 return dn_str
452
453 def display_authz_dn(self, who=None, entry=None):
454 if who is None:
455 if hasattr(self.ls, 'who') and self.ls.who:
456 who = self.ls.who
457 entry = self.ls.user_entry
458 else:
459 return 'anonymous'
460 if ldap0.dn.is_dn(who):
461 # Fall-back is to display the DN
462 result = self.display_dn(who, links=False)
463 # Determine relevant templates dict
464 bound_as_templates = ldap0.cidict.CIDict(self.cfg_param('boundas_template', {}))
465 # Read entry if necessary
466 if entry is None:
467 read_attrs = set(['objectClass'])
468 for ocl in bound_as_templates.keys():
469 read_attrs.update(GrabKeys(bound_as_templates[ocl]).keys)
470 try:
471 user_res = self.ls.l.read_s(who, attrlist=read_attrs)
472 except ldap0.LDAPError:
473 entry = None
474 else:
475 if user_res is None:
476 entry = {}
477 else:
478 entry = user_res.entry_as
479 if entry:
480 display_entry = DisplayEntry(self, self.dndndn, self.schema, entry, 'read_sep', True)
481 user_structural_oc = display_entry.entry.get_structural_oc()
482 for ocl in bound_as_templates.keys():
483 if self.schema.get_oid(ObjectClass, ocl) == user_structural_oc:
484 try:
485 result = bound_as_templates[ocl] % display_entry
486 except KeyError:
487 pass
488 else:
489 result = self.form.s2d(who)
490 return result
491 # end of display_authz_dn()
492
494 self,
495 title='',
496 message='',
497 main_div_id='Message',
498 main_menu_list=None,
499 context_menu_list=None,
500 ):
502 self,
503 title,
504 main_menu_list,
505 context_menu_list=context_menu_list,
506 main_div_id=main_div_id,
507 )
508 self.outf.write(message)
509 footer(self)
510 # end of simple_message()
511
512 def simple_msg(self, msg):
513 """
514 Output HTML text.
515 """
516 header(self, 'text/html', self.form.accept_charset)
517 self.outf.write(SIMPLE_MSG_HTML.format(message=msg))
518
520 self,
521 redirect_msg,
522 link_text='Continue&gt;&gt;',
523 refresh_time=3,
524 target_url=None,
525 ):
526 """
527 Outputs HTML text with redirecting <head> section.
528 """
529 if self.form is None:
530 self.form = Web2LDAPForm(None, self.env)
531 target_url = target_url or self.script_name
532 url_redirect_template_str = read_template(
533 self, None, 'redirect',
534 tmpl_filename=web2ldapcnf.redirect_template,
535 )
536 if refresh_time:
537 message_class = 'ErrorMessage'
538 else:
539 message_class = 'SuccessMessage'
540 header(self, 'text/html', self.form.accept_charset)
541 # Write out stub body with just a short redirect HTML snippet
542 self.outf.write(
543 url_redirect_template_str.format(
544 refresh_time=refresh_time,
545 target_url=target_url,
546 message_class=message_class,
547 redirect_msg=self.form.s2d(redirect_msg),
548 link_text=link_text,
549 )
550 )
551 # end of url_redirect()
552
553 def _new_session(self):
554 """
555 create new session
556 """
557 self.sid = self._session_store.new(self.env)
558 self.ls = LDAPSession(
559 get_remote_ip(self.env),
560 web2ldapcnf.ldap_trace_level,
561 web2ldapcnf.ldap_cache_ttl,
562 )
563 self.ls.cookie = self.form.set_cookie(self.sid, str(id(self.ls)))
564 self._session_store.save(self.sid, self.ls)
565 # end of _get_session()
566
567 def _get_session(self):
568 """
569 Restore old or initialize new web session object
570 """
571 if self.sid:
572 # Session ID given => try to restore old session
573 try:
574 last_session_timestamp, _ = self._session_store.sessiondict[self.sid]
575 except KeyError:
576 pass
577 self.ls = self._session_store.retrieve(self.sid, self.env)
578 if not isinstance(self.ls, LDAPSession):
580 if self.ls.cookie:
581 # Check whether HTTP_COOKIE contains the cookie of this particular session
582 cookie_name = ''.join((self.form.cookie_name_prefix, str(id(self.ls))))
583 if not (
584 cookie_name in self.form.cookies and
585 compare_digest(self.ls.cookie[cookie_name].value, self.form.cookies[cookie_name].value)
586 ):
587 raise WrongSessionCookie()
588 if web2ldapcnf.session_paranoid and \
589 self.current_access_time-last_session_timestamp > web2ldapcnf.session_paranoid:
590 # Store session with new session ID
591 self.sid = self._session_store.rename(self.sid, self.env)
592 else:
593 self.ls = None
594 # end of _get_session()
595
596 def _del_session(self):
597 """
598 delete the current session
599 """
600 self._session_store.delete(self.sid)
601 del self.ls
602 self.sid = self.ls = None
603 # end of _del_session()
604
605 def _handle_delsid(self):
606 """
607 if del_sid form parameter is present then delete the obsolete session
608 """
609 try:
610 del_sid = self.form.field['delsid'].value[0]
611 except IndexError:
612 return
613 try:
614 old_ls = self._session_store.retrieve(del_sid, self.env)
615 except SessionException:
616 pass
617 else:
618 # Remove session cookie
619 self.form.unset_cookie(old_ls.cookie)
620 # Explicitly remove old session
621 self._session_store.delete(del_sid)
622 # end of _handle_delsid()
623
625 """
626 Extract parameters either from LDAP URL in query string or real form input
627 """
628 if is_ldapurl(self.form.query_string):
629 # Extract the connection parameters from a LDAP URL
630 try:
631 input_ldapurl = ExtendedLDAPUrl(self.form.query_string)
632 except ValueError as err:
633 raise ErrorExit('Error parsing LDAP URL: %s.' % (
634 self.form.s2d(str(err))
635 ))
636 else:
637 self.command = self.command or SCOPE2COMMAND[input_ldapurl.scope]
638 if self.command in ('search', 'read'):
639 input_ldapurl.filterstr = input_ldapurl.filterstr or '(objectClass=*)'
640 # Re-instantiate form based on command derived from LDAP URL
641 self.form = FORM_CLASS.get(self.command, Web2LDAPForm)(self.inf, self.env)
642
643 else:
644 # Extract the connection parameters from form fields
645 self._handle_delsid()
646 if 'ldapurl' in self.form.input_field_names:
647 # One form parameter with LDAP URL
648 ldap_url_input = self.form.field['ldapurl'].value[0]
649 try:
650 input_ldapurl = ExtendedLDAPUrl(ldap_url_input)
651 except ValueError as err:
652 raise ErrorExit(
653 'Error parsing LDAP URL: %s.' % (err,)
654 )
655 else:
656 input_ldapurl = ExtendedLDAPUrl()
657 conntype = int(self.form.getInputValue('conntype', [0])[0])
658 input_ldapurl.urlscheme = CONNTYPE2URLSCHEME[conntype]
659 input_ldapurl.hostport = self.form.getInputValue('host', [None])[0]
660 input_ldapurl.start_tls = str(
661 START_TLS_REQUIRED * (conntype == 1)
662 )
663
664 # Separate parameters for dn, who, cred and scope
665 # have predecence over parameters specified in LDAP URL
666
667 dn = self.form.getInputValue('dn', [input_ldapurl.dn])[0]
668
669 who = self.form.getInputValue('who', [None])[0]
670 if who is None:
671 if input_ldapurl.who is not None:
672 who = input_ldapurl.who
673 else:
674 input_ldapurl.who = who
675
676 cred = self.form.getInputValue('cred', [None])[0]
677 if cred is None:
678 if input_ldapurl.cred is not None:
679 cred = input_ldapurl.cred
680 else:
681 input_ldapurl.cred = cred
682
683 assert isinstance(input_ldapurl.dn, str), TypeError(
684 "Type of 'input_ldapurl.dn' must be str, was %r" % (input_ldapurl.dn)
685 )
686 assert input_ldapurl.who is None or isinstance(input_ldapurl.who, str), TypeError(
687 "Type of 'input_ldapurl.who' must be str, was %r" % (input_ldapurl.who)
688 )
689 assert input_ldapurl.cred is None or isinstance(input_ldapurl.cred, str), TypeError(
690 "Type of 'input_ldapurl.cred' must be str, was %r" % (input_ldapurl.cred)
691 )
692 assert isinstance(dn, str), TypeError("Argument 'dn' must be str, was %r" % (dn))
693 assert who is None or isinstance(who, str), TypeError(
694 "Type of 'who' must be str, was %r" % (who)
695 )
696 assert cred is None or isinstance(cred, str), TypeError(
697 "Type of 'cred' must be str, was %r" % (cred)
698 )
699
700 if not ldap0.dn.is_dn(dn, flags=1):
701 raise ErrorExit('Invalid DN.')
702
703 scope_str = self.form.getInputValue(
704 'scope',
705 [
706 {False:str(input_ldapurl.scope), True:''}[input_ldapurl.scope is None]
707 ]
708 )[0]
709 if scope_str:
710 input_ldapurl.scope = int(scope_str)
711 else:
712 input_ldapurl.scope = None
713
714 return input_ldapurl, dn, who, cred
715 # end of _get_ldapconn_params()
716
717 def ldap_error_msg(self, ldap_err, template='{error_msg}<br>{matched_dn}'):
718 """
719 Converts a LDAPError exception into HTML error message
720
721 ldap_err
722 LDAPError instance
723 template
724 Raw binary string to be used as template
725 (must contain only a single placeholder)
726 """
727 matched_dn = None
728 if isinstance(ldap_err, ldap0.TIMEOUT) or not ldap_err.args:
729 error_msg = ''
730 elif isinstance(ldap_err, ldap0.INVALID_CREDENTIALS) and \
731 AD_LDAP49_ERROR_PREFIX in ldap_err.args[0].get('info', b''):
732 ad_error_code_pos = (
733 ldap_err.args[0]['info'].find(AD_LDAP49_ERROR_PREFIX)+len(AD_LDAP49_ERROR_PREFIX)
734 )
735 ad_error_code = int(ldap_err.args[0]['info'][ad_error_code_pos:ad_error_code_pos+3], 16)
736 error_msg = '%s:\n%s (%s)' % (
737 ldap_err.args[0]['desc'].decode(self.ls.charset),
738 ldap_err.args[0].get('info', b'').decode(self.ls.charset),
739 AD_LDAP49_ERROR_CODES.get(ad_error_code, 'unknown'),
740 )
741 else:
742 try:
743 error_desc = ldap_err.args[0]['desc'].decode(self.ls.charset)
744 error_info = ldap_err.args[0].get('info', b'').decode(self.ls.charset)
745 except UnicodeDecodeError:
746 error_msg = str(ldap_err)
747 except (TypeError, IndexError):
748 error_msg = str(ldap_err)
749 else:
750 error_msg = '{desc}: {info}'.format(
751 desc=error_desc,
752 info=error_info,
753 )
754
755 try:
756 matched_dn = ldap_err.args[0].get('matched', b'').decode(self.ls.charset)
757 except AttributeError:
758 matched_dn = None
759 error_msg = error_msg.replace('\r', '').replace('\t', '')
760 error_msg_html = self.form.s2d(error_msg, lf_entity='<br>')
761 # Add matchedDN to error message HTML if needed
762 if matched_dn:
763 matched_dn_html = '<br>Matched DN: %s' % (self.form.s2d(matched_dn))
764 else:
765 matched_dn_html = ''
766 return template.format(
767 error_msg=error_msg_html,
768 matched_dn=matched_dn_html
769 )
770
771 def run(self):
772 """
773 Really process the request
774 """
775 self.log(logging.DEBUG, 'Entering .run()')
776
777 # check for valid command
778 if self.command not in COMMAND_FUNCTION:
779
780 self.log(logging.WARN, 'Received invalid command %r', self.command)
781 self.url_redirect('Invalid web2ldap command')
782 return
783
784 # count command
785 COMMAND_COUNT[self.command or 'connect'] += 1
786
787 # initialize Form instance
788 self.form = FORM_CLASS.get(self.command, Web2LDAPForm)(self.inf, self.env)
789
790 #---------------------------------------------------------------
791 # try-except block for gracefully exception handling in the UI
792 #---------------------------------------------------------------
793
794 try:
795
796 if self.command in FORM_CLASS and not is_ldapurl(self.form.query_string):
797 # get the input fields
798 self.form.getInputFields()
799
800 # Check access here
801 if not self.check_access(self.command):
802 self.log(
803 logging.WARN,
804 'Access denied from %r to command %r',
805 self.env[web2ldapcnf.httpenv_remote_addr],
806 self.command,
807 )
808 raise ErrorExit('Access denied.')
809
810 # Handle simple early-exit commands
811 if self.command in {'', 'urlredirect', 'monitor', 'locate', 'metrics'}:
812 COMMAND_FUNCTION[self.command](self)
813 return
814
815 self._get_session()
816
817 if self.command == 'disconnect':
818 # Remove session cookie
819 self.form.unset_cookie(self.ls.cookie)
820 # Explicitly remove old session
821 self._session_store.delete(self.sid)
822 self.sid = None
823 # Redirect to start page to avoid people bookmarking disconnect URL
824 self.url_redirect('Disconnecting...', refresh_time=0)
825 return
826
827 self.ldap_url, self.dndndn, who, cred = self._get_ldapconn_params()
828
829 self.command = self.command or {
830 None: 'searchform',
831 ldap0.SCOPE_BASE: 'read',
832 ldap0.SCOPE_ONELEVEL: 'search',
833 ldap0.SCOPE_SUBTREE: 'search',
834 }[self.ldap_url.scope]
835
836 #-------------------------------------------------
837 # Connect to LDAP server
838 #-------------------------------------------------
839
840 if (
841 DNS_AVAIL
842 and self.ldap_url.hostport == ''
843 and self.ldap_url.urlscheme == 'ldap'
844 and (self.ls is None or self.ls.uri is None)
845 ):
846 # Force a SRV RR lookup for dc-style DNs,
847 # create list of URLs to connect to
848 dns_srv_rrs = dc_dn_lookup(self.dndndn)
849 init_uri_list = [
850 ExtendedLDAPUrl(urlscheme='ldap', hostport=host, dn=self.dndndn).connect_uri()
851 for host in dns_srv_rrs
852 ]
853 if not init_uri_list:
854 # No host specified in user's input
855 self._session_store.delete(self.sid)
856 self.sid = None
858 self,
859 h1_msg='Connect failed',
860 error_msg='No host specified.'
861 )
862 return
863 if len(init_uri_list) == 1:
864 init_uri = init_uri_list[0]
865 else:
866 # more than one possible servers => let user choose one
867 w2l_chasesrvrecord(self, init_uri_list)
868 return
869 elif self.ldap_url.hostport is not None:
870 init_uri = str(self.ldap_url.connect_uri()[:])
871 else:
872 init_uri = None
873
874 if init_uri and (
875 self.ls is None or self.ls.uri is None or init_uri != self.ls.uri
876 ):
877 # Delete current LDAPSession instance and create new
878 self._del_session()
879 self._new_session()
880 # Check whether access to target LDAP server is allowed
881 if web2ldapcnf.hosts.restricted_ldap_uri_list and \
882 init_uri not in LDAP_URI_LIST_CHECK_DICT:
883 raise ErrorExit('Only pre-configured LDAP servers allowed.')
884 # set this to make .cfg_param() retrieve correct site-specific config parameters
885 self.ls.uri = init_uri
886 # Connect to new specified host
887 self.ls.open(
888 init_uri,
889 self.cfg_param('timeout', LDAP_DEFAULT_TIMEOUT),
890 self.ldap_url.get_starttls_extop(
891 self.cfg_param('starttls', START_TLS_NO)
892 ),
893 self.env,
894 self.cfg_param('session_track_control', 0),
895 tls_options=self.cfg_param('tls_options', {}),
896 )
897 # Set host-/backend-specific timeout
898 self.ls.l.timeout = self.cfg_param('timeout', 60)
899 # Store session data in case anything goes wrong after here
900 # to give the exception handler a good chance
901 self._session_store.save(self.sid, self.ls)
902
903 # from here the code assumes that there is a valid LDAPSession instance
904 if self.ls is None:
905 # probably somebody entered invalid URL => exit here
906 self.url_redirect('No valid session!')
907 return
908
909 if self.ls.uri is None:
910 self._session_store.delete(self.sid)
911 self.sid = None
913 self,
914 h1_msg='Connect failed',
915 error_msg='No valid LDAP connection.'
916 )
917 return
918
919 # Store session data in case anything goes wrong after here
920 # to give the exception handler a good chance
921 self._session_store.save(self.sid, self.ls)
922 self.dndndn = self.dndndn
923
924 login_mech = self.form.getInputValue(
925 'login_mech',
926 [self.ldap_url.sasl_mech or '']
927 )[0].upper() or None
928
929 if (
930 who is not None and
931 cred is None and
932 (login_mech or '').encode('ascii') not in ldap0.sasl.SASL_NONINTERACTIVE_MECHS
933 ):
934 # first ask for password in a login form
935 w2l_login(
936 self,
937 login_msg='',
938 who=who, relogin=0, nomenu=1,
939 login_default_mech=self.ldap_url.sasl_mech
940 )
941 return
942
943 if (
944 (who is not None and cred is not None)
945 or
946 (
947 login_mech is not None
948 and
949 login_mech.encode('ascii') in ldap0.sasl.SASL_NONINTERACTIVE_MECHS
950 )
951 ):
952 self.dndndn = self.dndndn
953 # real bind operation
954 login_search_root = self.form.getInputValue(
955 'login_search_root',
956 [self.naming_context],
957 )[0]
958 try:
959 self.ls.bind(
960 who,
961 cred or '',
962 login_mech,
963 ''.join((
964 self.form.getInputValue('login_authzid_prefix', [''])[0],
965 self.form.getInputValue(
966 'login_authzid',
967 [self.ldap_url.sasl_authzid or ''],
968 )[0],
969 )) or None,
970 self.form.getInputValue('login_realm', [self.ldap_url.sasl_realm])[0],
971 self.binddn_mapping,
972 loginSearchRoot=login_search_root,
973 )
974 except ldap0.NO_SUCH_OBJECT as err:
975 w2l_login(
976 self,
977 login_msg=self.ldap_error_msg(err),
978 who=who, relogin=True
979 )
980 return
981 else:
982 # anonymous access
983 self.ls.init_rootdse()
984
985 # Check for valid LDAPSession and connection to provide reasonable
986 # error message instead of logging exception in case user is playing
987 # with manually generated URLs
988 if not isinstance(self.ls, LDAPSession) or self.ls.uri is None:
989 self.url_redirect('No valid LDAP connection!')
990 return
991 # Store session data in case anything goes wrong after here
992 # to give the exception handler a good chance
993 self._session_store.save(self.sid, self.ls)
994
995 # trigger update of various DN-related class properties
996 self.dndndn = self.dndndn
997
998 # Execute the command module
999 try:
1000 self.dispatch()
1001 except ldap0.SERVER_DOWN:
1002 # Try to reconnect to LDAP server and retry action
1003 self.ls.l.reconnect(self.ls.uri)
1004 self.dispatch()
1005 else:
1006 # Store current session
1007 self._session_store.save(self.sid, self.ls)
1008
1009 except FormException as form_error:
1010 log_exception(self.env, self.ls, self.dndndn, web2ldapcnf.log_error_details)
1012 self,
1013 'Error parsing form',
1014 'Error parsing form:<br>%s' % (
1015 self.form.s2d(str(form_error)),
1016 ),
1017 )
1018
1019 except ldap0.SERVER_DOWN as err:
1020 # Server is down and reconnecting impossible => remove session
1021 self._session_store.delete(self.sid)
1022 self.sid = None
1023 # Redirect to entry page
1025 self,
1026 h1_msg='Connect failed',
1027 error_msg='Connecting to %s impossible!<br>%s' % (
1028 self.form.s2d(init_uri or '-'),
1029 self.ldap_error_msg(err)
1030 )
1031 )
1032
1033 except ldap0.NO_SUCH_OBJECT as ldap_err:
1034
1035 if DNS_AVAIL:
1036 # first try to lookup dc-style DN via DNS
1037 host_list = dc_dn_lookup(self.dndndn)
1038 self.log(logging.DEBUG, 'host_list = %r', host_list)
1039 if host_list and ExtendedLDAPUrl(self.ls.uri).hostport not in host_list:
1040 # Found LDAP server for this naming context via DNS SRV RR
1041 w2l_chasesrvrecord(self, host_list)
1042 return
1043
1044 # Normal error handling
1045 log_exception(self.env, self.ls, self.dndndn, web2ldapcnf.log_error_details)
1046 failed_dn = self.dndndn
1047 if 'matched' in ldap_err.args[0]:
1048 self.dndndn = ldap_err.args[0]['matched'].decode(self.ls.charset)
1050 self,
1051 'No such object',
1052 self.ldap_error_msg(
1053 ldap_err,
1054 template='{{error_msg}}<br>{0}{{matched_dn}}'.format(
1055 self.display_dn(failed_dn)
1056 )
1057 )
1058 )
1059
1060 except (ldap0.PARTIAL_RESULTS, ldap0.REFERRAL) as err:
1061 w2l_chasereferral(self, err)
1062
1063 except InvalidSimpleBindDN as err:
1064 w2l_login(
1065 self,
1066 who='',
1067 login_msg=self.form.s2d(str(err)),
1068 relogin=True,
1069 )
1070
1071 except (
1072 ldap0.INSUFFICIENT_ACCESS,
1073 ldap0.STRONG_AUTH_REQUIRED,
1074 ldap0.INAPPROPRIATE_AUTH,
1075 ldap0.INVALID_CREDENTIALS,
1076 ) as err:
1077 log_exception(self.env, self.ls, self.dndndn, web2ldapcnf.log_error_details)
1078 w2l_login(
1079 self,
1080 login_msg=self.ldap_error_msg(err),
1081 who=who,
1082 relogin=True,
1083 )
1084
1085 except PasswordPolicyExpirationWarning as err:
1086 # Setup what's required for executing command 'passwd'
1087 self.dndndn = self.ls.l.whoami_s()[3:] or err.who
1088 # Output the change password form
1089 passwd.passwd_form(
1090 self,
1091 '',
1092 self.dndndn,
1093 None,
1094 'Password change needed',
1095 self.form.s2d(
1096 'Password will expire in %s!' % (
1097 ts2repr(
1098 Timespan.time_divisors,
1099 ' ',
1100 err.timeBeforeExpiration,
1101 )
1102 )
1103 ),
1104 )
1105
1106 except PasswordPolicyException as err:
1107 # Setup what's required for executing command 'passwd'
1108 self.dndndn = self.ls.l.get_whoami_dn() or err.who
1109 # Output the change password form
1110 passwd.passwd_form(
1111 self,
1112 '',
1113 self.dndndn,
1114 None,
1115 'Password change needed',
1116 self.form.s2d(err.desc)
1117 )
1118
1119 except (socket.error, socket.gaierror, IOError, UnicodeError) as err:
1120 log_exception(self.env, self.ls, self.dndndn, web2ldapcnf.log_error_details)
1122 self,
1123 'Unhandled %s' % err.__class__.__name__,
1124 self.form.s2d(str(err)),
1125 )
1126
1127 except ldap0.LDAPError as ldap_err:
1128 log_exception(self.env, self.ls, self.dndndn, web2ldapcnf.log_error_details)
1130 self,
1131 'Unhandled %s' % ldap_err.__class__.__name__,
1132 self.ldap_error_msg(ldap_err),
1133 )
1134
1135 except ErrorExit as error_exit:
1136 self.log(logging.WARN, 'ErrorExit: %r', error_exit.error_message)
1137 exception_message(self, 'Error', error_exit.error_message)
1138
1139 except MaxSessionPerIPExceeded as session_err:
1140 self.log(logging.WARN, str(session_err))
1141 self.simple_msg(
1142 'Client %s exceeded limit of max. %d sessions! Try later...' % (
1143 session_err.remote_ip,
1144 session_err.max_session_count,
1145 )
1146 )
1147
1148 except MaxSessionCountExceeded as session_err:
1149 self.log(logging.WARN, str(session_err))
1150 self.simple_msg('Too many web sessions! Try later...')
1151
1152 except SessionException:
1153 if self.command == 'disconnect':
1154 # Probably already disconnected => ignore exception and redirect to start page
1155 self.url_redirect('Disconnecting...', refresh_time=0)
1156 else:
1157 log_exception(self.env, self.ls, self.dndndn, web2ldapcnf.log_error_details)
1158 self.url_redirect('Session handling error.')
1159
1160 except Exception:
1161 if hasattr(self, 'ls'):
1162 error_ls = self.ls
1163 else:
1164 error_ls = None
1165 log_exception(self.env, error_ls, self.dndndn, web2ldapcnf.log_error_details)
1166 self.simple_msg('Unhandled error!')
1167
1168 # end of run()
def ldap_url_anchor(self, data)
Definition: handler.py:329
def display_dn(self, dn, links=False)
Definition: handler.py:441
def __init__(self, env, outf)
Definition: handler.py:168
def check_access(self, command)
Definition: handler.py:266
def display_authz_dn(self, who=None, entry=None)
Definition: handler.py:453
def ldap_error_msg(self, ldap_err, template='{error_msg}< br >{matched_dn}')
Definition: handler.py:717
def cfg_param(self, param_key, default)
Definition: handler.py:247
def simple_message(self, title='', message='', main_div_id='Message', main_menu_list=None, context_menu_list=None)
Definition: handler.py:500
def anchor(self, command, anchor_text, form_parameters, target=None, title=None, anchor_id=None, rel=None)
Definition: handler.py:289
def begin_form(self, command, method, target=None, enctype='application/x-www-form-urlencoded')
Definition: handler.py:359
def url_redirect(self, redirect_msg, link_text='Continue &gt;&gt;', refresh_time=3, target_url=None)
Definition: handler.py:525
def form_html(self, command, submitstr, method, form_parameters, extrastr='', target=None)
Definition: handler.py:380
def log(self, level, msg, *args, **kwargs)
Definition: log.py:53
def w2l_connect(app, h1_msg='Connect', error_msg='')
Definition: connect.py:27
def top_section(app, title, main_menu_list, context_menu_list=None, main_div_id='Message')
Definition: gui.py:352
def exception_message(app, h1_msg, error_msg)
Definition: gui.py:585
def header(app, content_type, charset, more_headers=None)
Definition: gui.py:459
def footer(app)
Definition: gui.py:477
def w2l_login(app, title_msg='Bind', login_msg='', who='', relogin=False, nomenu=False, login_default_mech=None)
Definition: login.py:29
def w2l_chasereferral(app, ref_exc)
Definition: referral.py:28
def w2l_chasesrvrecord(app, host_list)
Definition: srvrr.py:53
def read_template(app, config_key, form_desc='', tmpl_filename=None)
Definition: tmpl.py:40
def dc_dn_lookup(dn)
Definition: dns.py:56
def log_exception(env, ls, dn, debug)
Definition: log.py:57
str ts2repr(Sequence[Tuple[str, int]] time_divisors, str ts_sep, Union[str, bytes] ts_value)
Definition: utctime.py:79
def get_remote_ip(env)
Definition: helper.py:25