1# -*- coding: ascii -*- 2""" 3web2ldap.app.form: class for web2ldap input form handling 4 5web2ldap - a web-based LDAP Client, 6see https://www.web2ldap.de for details 7 8(c) 1998-2021 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 http.cookies 16 17import ldap0.ldif 18import ldap0.schema 19from ldap0.pw import random_string 20 21import web2ldapcnf 22 23from ..ldapsession import AVAILABLE_BOOLEAN_CONTROLS, CONTROL_TREEDELETE 24from ..web import HTML_ESCAPE_MAP 25from ..ldaputil import RDN_PATTERN, ATTR_TYPE_PATTERN 26from ..ldaputil.oidreg import OID_REG 27from ..ldaputil.passwd import AVAIL_USERPASSWORD_SCHEMES 28from .gui import HIDDEN_FIELD 29from .searchform import ( 30 SEARCH_OPTIONS, 31 SEARCH_SCOPE_OPTIONS, 32 SEARCH_SCOPE_STR_SUBTREE, 33) 34from ..web.forms import ( 35 Input, 36 Field, 37 Textarea, 38 BytesInput, 39 Select, 40 Checkbox, 41 Form, 42 InvalidValueFormat, 43) 44from ..web.session import SESSION_ID_CHARS, SESSION_ID_LENGTH, SESSION_ID_REGEX 45 46 47# Work around https://bugs.python.org/issue29613 48http.cookies.Morsel._reserved['samesite'] = 'SameSite' 49 50class Web2LDAPForm(Form): 51 """ 52 Form sub-class for a web2ldap use-case 53 54 more sub-classes define forms for different URL commands 55 """ 56 command = None 57 cookie_length = web2ldapcnf.cookie_length or 2 * 42 58 cookie_max_age = web2ldapcnf.cookie_max_age 59 cookie_domain = web2ldapcnf.cookie_domain 60 cookie_name_prefix = 'web2ldap_' 61 62 def __init__(self, inf, env): 63 Form.__init__(self, inf, env) 64 # Cookie handling 65 try: 66 self.cookies = http.cookies.SimpleCookie(self.env['HTTP_COOKIE']) 67 except KeyError: 68 self.cookies = http.cookies.SimpleCookie() 69 self.next_cookie = http.cookies.SimpleCookie() 70 71 @staticmethod 72 def s2d( 73 value, 74 tab_identiation='', 75 sp_entity=' ', 76 lf_entity='\n' 77 ): 78 assert isinstance(value, str), \ 79 TypeError('Argument value must be str, was %r' % (value,)) 80 value = value or '' 81 translate_map = dict(HTML_ESCAPE_MAP.items()) 82 translate_map.update({ 83 9: tab_identiation, 84 10: lf_entity, 85 }) 86 return value.translate(translate_map).replace(' ', sp_entity) 87 88 def unset_cookie(self, cki): 89 if cki is not None: 90 assert len(cki) == 1, \ 91 ValueError( 92 'More than one Morsel cookie instance in cki: %d objects found' % (len(cki)) 93 ) 94 cookie_name = list(cki.keys())[0] 95 cki[cookie_name] = '' 96 cki[cookie_name]['max-age'] = 0 97 self.next_cookie.update(cki) 98 # end of unset_cookie() 99 100 def get_cookie_domain(self): 101 if self.cookie_domain: 102 cookie_domain = self.cookie_domain 103 elif 'SERVER_NAME' in self.env or 'HTTP_HOST' in self.env: 104 cookie_domain = self.env.get('HTTP_HOST', self.env['SERVER_NAME']).split(':')[0] 105 return cookie_domain 106 107 def set_cookie(self, name_suffix): 108 # Generate a randomized key and value 109 cookie_key = random_string( 110 alphabet=SESSION_ID_CHARS, 111 length=self.cookie_length, 112 ) 113 cookie_name = ''.join((self.cookie_name_prefix, name_suffix)) 114 cki = http.cookies.SimpleCookie({ 115 cookie_name: cookie_key, 116 }) 117 cki[cookie_name]['path'] = self.script_name 118 cki[cookie_name]['domain'] = self.get_cookie_domain() 119 cki[cookie_name]['max-age'] = str(self.cookie_max_age) 120 cki[cookie_name]['httponly'] = True 121 cki[cookie_name]['samesite'] = 'Strict' 122 if self.env.get('HTTPS', None) == 'on': 123 cki[cookie_name]['secure'] = True 124 self.next_cookie.update(cki) 125 return cki # set_cookie() 126 127 def fields(self): 128 return [ 129 Input( 130 'delsid', 131 'Old SID to be deleted', 132 SESSION_ID_LENGTH, 133 1, 134 SESSION_ID_REGEX, 135 ), 136 Input('who', 'Bind DN/AuthcID', 1000, 1, '.*', size=40), 137 Input('cred', 'with Password', 200, 1, '.*', size=15), 138 Select( 139 'login_authzid_prefix', 140 'SASL AuthzID', 141 1, 142 options=[('', '- no prefix -'), ('u:', 'user-ID'), ('dn:', 'DN')], 143 default=None 144 ), 145 Input('login_authzid', 'SASL AuthzID', 1000, 1, '.*', size=20), 146 Input('login_realm', 'SASL Realm', 1000, 1, '.*', size=20), 147 AuthMechSelect('login_mech', 'Authentication mechanism'), 148 Input('ldapurl', 'LDAP Url', 1024, 1, '[ ]*ldap(|i|s)://.*', size=30), 149 Input( 150 'host', 'Host:Port', 151 255, 1, 152 '[a-zA-Z0-9_.:\\[\\]-]+', 153 size=70, 154 ), 155 DistinguishedNameInput('dn', 'Distinguished Name'), 156 Select( 157 'scope', 'Scope', 1, 158 options=SEARCH_SCOPE_OPTIONS, 159 default=SEARCH_SCOPE_STR_SUBTREE, 160 ), 161 DistinguishedNameInput('login_search_root', 'Login search root'), 162 Select( 163 'conntype', 'Connection type', 1, 164 options=[ 165 ('0', 'LDAP clear-text connection'), 166 ('1', 'LDAP with StartTLS ext.op.'), 167 ('2', 'LDAP over separate SSL port (LDAPS)'), 168 ('3', 'LDAP over Unix domain socket (LDAPI)') 169 ], 170 default='0', 171 ) 172 ] 173 174 def action_url(self, command, sid): 175 return '%s/%s%s' % ( 176 self.script_name, 177 command, 178 {False:'/%s' % sid, True:''}[sid is None], 179 ) 180 181 def begin_form( 182 self, 183 command, 184 sid, 185 method, 186 target=None, 187 enctype='application/x-www-form-urlencoded', 188 ): 189 target = { 190 False:'target="%s"' % (target), 191 True:'', 192 }[target is None] 193 return """ 194 <form 195 action="%s" 196 method="%s" 197 %s 198 enctype="%s" 199 accept-charset="%s" 200 > 201 """ % ( 202 self.action_url(command, sid), 203 method, 204 target, 205 enctype, 206 self.accept_charset 207 ) 208 209 def hidden_field_html(self, name, value, desc): 210 return HIDDEN_FIELD % ( 211 name, 212 self.s2d(value, sp_entity=' '), 213 self.s2d(desc, sp_entity=' '), 214 ) 215 216 def hidden_input_html(self, ignored_fields=None): 217 """ 218 Return all input parameters as hidden fields in one HTML string. 219 220 ignored_fields 221 Names of parameters to be excluded. 222 """ 223 ignored_fields = set(ignored_fields or []) 224 res = [] 225 for fname in self.input_field_names: 226 if fname in ignored_fields: 227 continue 228 for val in self.field[fname].value: 229 if isinstance(val, str): 230 res.append(self.hidden_field_html(fname, val, '')) 231 else: 232 res.append(self.hidden_field_html(fname, '', '')) 233 return '\n'.join(res) 234 235 236class SearchAttrs(Input): 237 238 def __init__(self, name='search_attrs', text='Attributes to be read'): 239 Input.__init__(self, name, text, 1000, 1, '[@*+0-9.\\w,_;-]+') 240 241 def set_value(self, value): 242 value = ','.join( 243 filter( 244 None, 245 map(str.strip, value.replace(' ', ',').split(',')) 246 ) 247 ) 248 Input.set_value(self, value) 249 250 251class Web2LDAPFormSearchform(Web2LDAPForm): 252 command = 'searchform' 253 254 def fields(self): 255 res = Web2LDAPForm.fields(self) 256 res.extend([ 257 Input( 258 'search_submit', 'Search form submit button', 259 6, 1, 260 '(Search|[+-][0-9]+)', 261 ), 262 Select( 263 'searchform_mode', 264 'Search form mode', 265 1, 266 options=[('base', 'Base'), ('adv', 'Advanced'), ('exp', 'Expert')], 267 default='base', 268 ), 269 DistinguishedNameInput('search_root', 'Search root'), 270 Input( 271 'filterstr', 272 'Search filter string', 273 1200, 274 1, 275 '.*', 276 size=90, 277 ), 278 Input( 279 'searchform_template', 280 'Search form template name', 281 60, 282 web2ldapcnf.max_searchparams, 283 '[a-zA-Z0-9. ()_-]+', 284 ), 285 Select( 286 'search_resnumber', 'Number of results to display', 1, 287 options=[ 288 ('0', 'unlimited'), ('10', '10'), ('20', '20'), 289 ('50', '50'), ('100', '100'), ('200', '200'), 290 ], 291 default='10' 292 ), 293 Select( 294 'search_lastmod', 'Interval of last creation/modification', 1, 295 options=[ 296 ('-1', '-'), 297 ('10', '10 sec.'), 298 ('60', '1 min.'), 299 ('600', '10 min.'), 300 ('3600', '1 hour'), 301 ('14400', '4 hours'), 302 ('43200', '12 hours'), 303 ('86400', '24 hours'), 304 ('172800', '2 days'), 305 ('604800', '1 week'), 306 ('2419200', '4 weeks'), 307 ('6048000', '10 weeks'), 308 ('31536000', '1 year'), 309 ], 310 default='-1' 311 ), 312 InclOpAttrsCheckbox(default='yes', checked=False), 313 Select('search_mode', 'Search Mode', 1, options=['(&%s)', '(|%s)']), 314 Input( 315 'search_attr', 316 'Attribute(s) to be searched', 317 100, 318 web2ldapcnf.max_searchparams, 319 '[\\w,_;-]+', 320 ), 321 Input( 322 'search_mr', 'Matching Rule', 323 100, 324 web2ldapcnf.max_searchparams, 325 '[\\w,_;-]+', 326 ), 327 Select( 328 'search_option', 'Search option', 329 web2ldapcnf.max_searchparams, 330 options=SEARCH_OPTIONS, 331 ), 332 Input( 333 'search_string', 'Search string', 334 600, 335 web2ldapcnf.max_searchparams, 336 '.*', 337 size=60, 338 ), 339 SearchAttrs(), 340 ExportFormatSelect(), 341 ]) 342 return res 343 344 345class Web2LDAPFormSearch(Web2LDAPFormSearchform): 346 command = 'search' 347 348 def fields(self): 349 res = Web2LDAPFormSearchform.fields(self) 350 res.extend([ 351 Input( 352 'search_resminindex', 353 'Minimum index of search results', 354 10, 1, 355 '[0-9]+', 356 ), 357 Input( 358 'search_resnumber', 359 'Number of results to display', 360 3, 1, 361 '[0-9]+', 362 ), 363 ]) 364 return res 365 366 367class Web2LDAPFormConninfo(Web2LDAPForm): 368 command = 'conninfo' 369 370 def fields(self): 371 res = Web2LDAPForm.fields(self) 372 res.append( 373 Select( 374 'conninfo_flushcaches', 375 'Flush caches', 376 1, 377 options=('0', '1'), 378 default='0', 379 ) 380 ) 381 return res 382 383 384class Web2LDAPFormParams(Web2LDAPForm): 385 command = 'params' 386 387 def fields(self): 388 res = Web2LDAPForm.fields(self) 389 res.extend([ 390 Select( 391 'params_all_controls', 392 'List all controls', 393 1, 394 options=('0', '1'), 395 default='0', 396 ), 397 Input( 398 'params_enable_control', 399 'Enable LDAPv3 Boolean Control', 400 50, 1, 401 '([0-9]+.)*[0-9]+', 402 ), 403 Input( 404 'params_disable_control', 405 'Disable LDAPv3 Boolean Control', 406 50, 1, 407 '([0-9]+.)*[0-9]+', 408 ), 409 Select( 410 'ldap_deref', 411 'Dereference aliases', 412 maxValues=1, 413 default=str(ldap0.DEREF_NEVER), 414 options=[ 415 (str(ldap0.DEREF_NEVER), 'never'), 416 (str(ldap0.DEREF_SEARCHING), 'searching'), 417 (str(ldap0.DEREF_FINDING), 'finding'), 418 (str(ldap0.DEREF_ALWAYS), 'always'), 419 ] 420 ), 421 ]) 422 return res 423 424 425class Web2LDAPFormInput(Web2LDAPForm): 426 427 """Base class for entry data input not directly used""" 428 def fields(self): 429 res = Web2LDAPForm.fields(self) 430 res.extend([ 431 Input('in_oc', 'Object classes', 60, 40, '[a-zA-Z0-9.-]+'), 432 Select( 433 'in_ft', 'Type of input form', 434 1, 435 options=('Template', 'Table', 'LDIF', 'OC'), 436 default='Template', 437 ), 438 Input( 439 'in_mr', 440 'Add/del row', 441 8, 1, 442 '(Template|Table|LDIF|[+-][0-9]+)', 443 ), 444 Select( 445 'in_oft', 'Type of input form', 446 1, 447 options=('Template', 'Table', 'LDIF'), 448 default='Template', 449 ), 450 AttributeType('in_at', 'Attribute type', web2ldapcnf.input_maxattrs), 451 AttributeType('in_avi', 'Value index', web2ldapcnf.input_maxattrs), 452 BytesInput( 453 'in_av', 'Attribute Value', 454 web2ldapcnf.input_maxfieldlen, 455 web2ldapcnf.input_maxattrs, 456 None 457 ), 458 LDIFTextArea('in_ldif', 'LDIF data'), 459 Select( 460 'in_ocf', 'Object class form mode', 1, 461 options=[ 462 ('tmpl', 'LDIF templates'), 463 ('exp', 'Object class selection') 464 ], 465 default='tmpl', 466 ), 467 ]) 468 return res 469 470 471class Web2LDAPFormAdd(Web2LDAPFormInput): 472 command = 'add' 473 474 def fields(self): 475 res = Web2LDAPFormInput.fields(self) 476 res.extend([ 477 Input('add_rdn', 'RDN of new entry', 255, 1, '.*', size=50), 478 DistinguishedNameInput('add_clonedn', 'DN of template entry'), 479 Input( 480 'add_template', 'LDIF template name', 481 60, 482 web2ldapcnf.max_searchparams, 483 '.+', 484 ), 485 Input('add_basedn', 'Base DN of new entry', 1024, 1, '.*', size=50), 486 ]) 487 return res 488 489 490class Web2LDAPFormModify(Web2LDAPFormInput): 491 command = 'modify' 492 493 def fields(self): 494 res = Web2LDAPFormInput.fields(self) 495 res.extend([ 496 AttributeType( 497 'in_oldattrtypes', 498 'Old attribute types', 499 web2ldapcnf.input_maxattrs, 500 ), 501 AttributeType( 502 'in_roattroids', 503 'Read-only attribute types', 504 web2ldapcnf.input_maxattrs, 505 ), 506 Input( 507 'in_assertion', 508 'Assertion filter string', 509 2000, 510 1, 511 '.*', 512 required=False, 513 ), 514 ]) 515 return res 516 517 518class Web2LDAPFormDds(Web2LDAPForm): 519 command = 'dds' 520 521 def fields(self): 522 res = Web2LDAPForm.fields(self) 523 res.extend([ 524 Input( 525 'dds_renewttlnum', 526 'Request TTL number', 527 12, 1, 528 '[0-9]+', 529 default=None, 530 ), 531 Select( 532 'dds_renewttlfac', 533 'Request TTL factor', 534 1, 535 options=( 536 ('1', 'seconds'), 537 ('60', 'minutes'), 538 ('3600', 'hours'), 539 ('86400', 'days'), 540 ), 541 default='1' 542 ), 543 ]) 544 return res 545 546 547class Web2LDAPFormBulkmod(Web2LDAPForm): 548 command = 'bulkmod' 549 550 def fields(self): 551 res = Web2LDAPForm.fields(self) 552 bulkmod_ctrl_options = [ 553 (control_oid, OID_REG.get(control_oid, (control_oid,))[0]) 554 for control_oid, control_spec in AVAILABLE_BOOLEAN_CONTROLS.items() 555 if ( 556 '**all**' in control_spec[0] 557 or '**write**' in control_spec[0] 558 or 'modify' in control_spec[0] 559 ) 560 ] 561 res.extend([ 562 Input( 563 'bulkmod_submit', 564 'Search form submit button', 565 6, 1, 566 '(Next>>|<<Back|Apply|Cancel|[+-][0-9]+)', 567 ), 568 Select( 569 'bulkmod_ctrl', 570 'Extended controls', 571 len(bulkmod_ctrl_options), 572 options=bulkmod_ctrl_options, 573 default=None, 574 size=min(8, len(bulkmod_ctrl_options)), 575 multiSelect=1, 576 ), 577 Input( 578 'filterstr', 579 'Search filter string for searching entries to be deleted', 580 1200, 1, 581 '.*', 582 ), 583 Input( 584 'bulkmod_modrow', 585 'Add/del row', 586 8, 1, 587 '(Template|Table|LDIF|[+-][0-9]+)', 588 ), 589 AttributeType('bulkmod_at', 'Attribute type', web2ldapcnf.input_maxattrs), 590 Select( 591 'bulkmod_op', 592 'Modification type', 593 web2ldapcnf.input_maxattrs, 594 options=( 595 ('', ''), 596 (str(ldap0.MOD_ADD), 'add'), 597 (str(ldap0.MOD_DELETE), 'delete'), 598 (str(ldap0.MOD_REPLACE), 'replace'), 599 (str(ldap0.MOD_INCREMENT), 'increment'), 600 ), 601 default=None, 602 ), 603 BytesInput( 604 'bulkmod_av', 'Attribute Value', 605 web2ldapcnf.input_maxfieldlen, 606 web2ldapcnf.input_maxattrs, 607 None, 608 size=30, 609 ), 610 DistinguishedNameInput('bulkmod_newsuperior', 'New superior DN'), 611 Checkbox('bulkmod_cp', 'Copy entries', 1, default='yes', checked=False), 612 ]) 613 return res 614 615 616class Web2LDAPFormDelete(Web2LDAPForm): 617 command = 'delete' 618 619 def fields(self): 620 res = Web2LDAPForm.fields(self) 621 delete_ctrl_options = [ 622 (control_oid, OID_REG.get(control_oid, (control_oid,))[0]) 623 for control_oid, control_spec in AVAILABLE_BOOLEAN_CONTROLS.items() 624 if ( 625 '**all**' in control_spec[0] 626 or '**write**' in control_spec[0] 627 or 'delete' in control_spec[0] 628 ) 629 ] 630 delete_ctrl_options.append((CONTROL_TREEDELETE, 'Tree Deletion')) 631 res.extend([ 632 Select( 633 'delete_confirm', 'Confirmation', 634 1, 635 options=('yes', 'no'), 636 default='no', 637 ), 638 Select( 639 'delete_ctrl', 640 'Extended controls', 641 len(delete_ctrl_options), 642 options=delete_ctrl_options, 643 default=None, 644 size=min(8, len(delete_ctrl_options)), 645 multiSelect=1, 646 ), 647 Input( 648 'filterstr', 649 'Search filter string for searching entries to be deleted', 650 1200, 1, 651 '.*', 652 ), 653 Input('delete_attr', 'Attribute to be deleted', 255, 100, '[\\w_;-]+'), 654 ]) 655 return res 656 657 658class Web2LDAPFormRename(Web2LDAPForm): 659 command = 'rename' 660 661 def fields(self): 662 res = Web2LDAPForm.fields(self) 663 res.extend([ 664 Input( 665 'rename_newrdn', 666 'New RDN', 667 255, 1, 668 RDN_PATTERN, 669 size=50, 670 ), 671 DistinguishedNameInput('rename_newsuperior', 'New superior DN'), 672 Checkbox('rename_delold', 'Delete old', 1, default='yes', checked=True), 673 Input( 674 'rename_newsupfilter', 675 'Filter string for searching new superior entry', 300, 1, '.*', 676 default='(|(objectClass=organization)(objectClass=organizationalUnit))', 677 size=50, 678 ), 679 DistinguishedNameInput( 680 'rename_searchroot', 681 'Search root under which to look for new superior entry.', 682 ), 683 Input( 684 'rename_supsearchurl', 685 'LDAP URL for searching new superior entry', 686 100, 1, 687 '.*', 688 size=30, 689 ), 690 ]) 691 return res 692 693 694class Web2LDAPFormPasswd(Web2LDAPForm): 695 command = 'passwd' 696 passwd_actions = ( 697 ( 698 'passwdextop', 699 'Server-side', 700 'Password modify extended operation', 701 ), 702 ( 703 'setuserpassword', 704 'Modify password attribute', 705 'Set the password attribute with modify operation' 706 ), 707 ) 708 709 @staticmethod 710 def passwd_fields(): 711 """ 712 return list of Field instances needed for a password change input form 713 """ 714 return [ 715 Select( 716 'passwd_action', 'Password action', 1, 717 options=[ 718 (action, short_desc) 719 for action, short_desc, _ in Web2LDAPFormPasswd.passwd_actions 720 ], 721 default='setuserpassword' 722 ), 723 DistinguishedNameInput('passwd_who', 'Password DN'), 724 Field('passwd_oldpasswd', 'Old password', 100, 1, '.*'), 725 Field('passwd_newpasswd', 'New password', 100, 2, '.*'), 726 Select( 727 'passwd_scheme', 'Password hash scheme', 1, 728 options=AVAIL_USERPASSWORD_SCHEMES.items(), 729 default=None, 730 ), 731 Checkbox( 732 'passwd_ntpasswordsync', 733 'Sync ntPassword for Samba', 734 1, 735 default='yes', 736 checked=True, 737 ), 738 Checkbox( 739 'passwd_settimesync', 740 'Sync password setting times', 741 1, 742 default='yes', 743 checked=True, 744 ), 745 Checkbox( 746 'passwd_forcechange', 747 'Force password change', 748 1, 749 default='yes', 750 checked=False, 751 ), 752 Checkbox( 753 'passwd_inform', 754 'Password change inform action', 755 1, 756 default="display_url", 757 checked=False, 758 ), 759 ] 760 761 def fields(self): 762 res = Web2LDAPForm.fields(self) 763 res.extend(self.passwd_fields()) 764 return res 765 766 767class Web2LDAPFormRead(Web2LDAPForm): 768 command = 'read' 769 770 def fields(self): 771 res = Web2LDAPForm.fields(self) 772 res.extend([ 773 Input( 774 'filterstr', 775 'Search filter string when reading single entry', 776 1200, 1, 777 '.*', 778 ), 779 Select( 780 'read_nocache', 'Force fresh read', 781 1, 782 options=['0', '1'], 783 default='0', 784 ), 785 Input('read_attr', 'Read attribute', 255, 100, '[\\w_;-]+'), 786 Input('read_attrindex', 'Read attribute', 255, 1, '[0-9]+'), 787 Input('read_attrmimetype', 'MIME type', 255, 1, '[\\w.-]+/[\\w.-]+'), 788 Select( 789 'read_output', 'Read output format', 790 1, 791 options=('table', 'vcard', 'template'), 792 default='template', 793 ), 794 SearchAttrs(), 795 Input('read_expandattr', 'Attributes to be read', 1000, 50, '[*+\\w,_;-]+'), 796 ]) 797 return res 798 799 800class Web2LDAPFormGroupadm(Web2LDAPForm): 801 command = 'groupadm' 802 803 def fields(self): 804 res = Web2LDAPForm.fields(self) 805 res.extend([ 806 DistinguishedNameInput('groupadm_searchroot', 'Group search root'), 807 Input('groupadm_name', 'Group name', 100, 1, '.*', size=30), 808 DistinguishedNameInput('groupadm_add', 'Add to group', 300), 809 DistinguishedNameInput('groupadm_remove', 'Remove from group', 300), 810 Select( 811 'groupadm_view', 812 'Group list view', 813 1, 814 options=( 815 ('0', 'none of the'), 816 ('1', 'only member'), 817 ('2', 'all'), 818 ), 819 default='1', 820 ), 821 ]) 822 return res 823 824 825class Web2LDAPFormLogin(Web2LDAPForm): 826 command = 'login' 827 828 def fields(self): 829 res = Web2LDAPForm.fields(self) 830 res.append( 831 DistinguishedNameInput('login_who', 'Bind DN') 832 ) 833 return res 834 835 836class Web2LDAPFormLocate(Web2LDAPForm): 837 command = 'locate' 838 839 def fields(self): 840 res = Web2LDAPForm.fields(self) 841 res.append( 842 Input('locate_name', 'Location name', 500, 1, '.*', size=25) 843 ) 844 return res 845 846 847class Web2LDAPFormOid(Web2LDAPForm): 848 command = 'oid' 849 850 def fields(self): 851 res = Web2LDAPForm.fields(self) 852 res.extend([ 853 OIDInput('oid', 'OID'), 854 Select( 855 'oid_class', 856 'Schema element class', 857 1, 858 options=ldap0.schema.SCHEMA_ATTRS, 859 default='', 860 ), 861 ]) 862 return res 863 864 865class Web2LDAPFormDit(Web2LDAPForm): 866 command = 'dit' 867 868 869class DistinguishedNameInput(Input): 870 """Input field class for LDAP DNs.""" 871 872 def __init__(self, name='dn', text='DN', maxValues=1, required=False, default=''): 873 Input.__init__( 874 self, name, text, 1024, maxValues, None, 875 size=70, required=required, default=default 876 ) 877 878 def _validate_format(self, value): 879 if value and not ldap0.dn.is_dn(value): 880 raise InvalidValueFormat(self.name, self.text, value) 881 882 883class LDIFTextArea(Textarea): 884 """A single multi-line input field for LDIF data""" 885 886 def __init__( 887 self, 888 name='in_ldif', 889 text='LDIF data', 890 required=False, 891 max_entries=1 892 ): 893 Textarea.__init__( 894 self, 895 name, 896 text, 897 web2ldapcnf.ldif_maxbytes, 898 1, 899 '^.*$', 900 required=required, 901 ) 902 self._max_entries = max_entries 903 904 @property 905 def ldif_records(self): 906 if self.value: 907 return list( 908 ldap0.ldif.LDIFParser.frombuf( 909 '\n'.join(self.value).encode(self.charset), 910 ignored_attr_types=[], 911 process_url_schemes=web2ldapcnf.ldif_url_schemes 912 ).parse(max_entries=self._max_entries) 913 ) 914 return [] 915 916 917class OIDInput(Input): 918 919 def __init__(self, name, text, default=None): 920 Input.__init__( 921 self, name, text, 922 512, 1, '[a-zA-Z0-9_.;*-]+', 923 default=default, 924 required=False, 925 size=30, 926 ) 927 928 929class ObjectClassSelect(Select): 930 """Select field class for choosing the object class(es)""" 931 932 def __init__( 933 self, 934 name='in_oc', 935 text='Object classes', 936 options=None, 937 default=None, 938 required=False, 939 accesskey='', 940 size=12, # Size of displayed select field 941 ): 942 select_default = default or [] 943 select_default.sort(key=str.lower) 944 additional_options = [ 945 opt 946 for opt in options or [] 947 if not opt in select_default 948 ] 949 additional_options.sort(key=str.lower) 950 select_options = select_default[:] 951 select_options.extend(additional_options) 952 Select.__init__( 953 self, 954 name, text, 955 maxValues=200, 956 required=required, 957 options=select_options, 958 default=select_default, 959 accesskey=accesskey, 960 size=size, 961 ignoreCase=1, 962 multiSelect=1 963 ) 964 self.set_regex('[\\w]+') 965 self.maxLen = 200 966 # end of ObjectClassSelect() 967 968 969class ExportFormatSelect(Select): 970 """Select field class for choosing export format""" 971 972 def __init__( 973 self, 974 default='ldif1', 975 required=False, 976 ): 977 Select.__init__( 978 self, 979 'search_output', 980 'Export format', 981 1, 982 options=( 983 ('table', 'Table/template'), 984 ('raw', 'Raw DN list'), 985 ('print', 'Printable'), 986 ('ldif', 'LDIF (Umich)'), 987 ('ldif1', 'LDIFv1 (RFC2849)'), 988 ('csv', 'CSV'), 989 ('excel', 'Excel'), 990 ), 991 default=default, 992 required=required, 993 size=1, 994 ) 995 996 997class AttributeType(Input): 998 """ 999 Input field for an LDAP attribute type 1000 """ 1001 def __init__(self, name, text, maxValues): 1002 Input.__init__( 1003 self, 1004 name, 1005 text, 1006 500, 1007 maxValues, 1008 ATTR_TYPE_PATTERN, 1009 required=False, 1010 size=30 1011 ) 1012 1013 1014class InclOpAttrsCheckbox(Checkbox): 1015 1016 def __init__(self, default='yes', checked=False): 1017 Checkbox.__init__( 1018 self, 1019 'search_opattrs', 1020 'Request operational attributes', 1021 1, 1022 default=default, 1023 checked=checked 1024 ) 1025 1026 1027class AuthMechSelect(Select): 1028 """Select field class for choosing the bind mech""" 1029 1030 supported_bind_mechs = { 1031 '': 'Simple Bind', 1032 'DIGEST-MD5': 'SASL Bind: DIGEST-MD5', 1033 'CRAM-MD5': 'SASL Bind: CRAM-MD5', 1034 'PLAIN': 'SASL Bind: PLAIN', 1035 'LOGIN': 'SASL Bind: LOGIN', 1036 'GSSAPI': 'SASL Bind: GSSAPI', 1037 'EXTERNAL': 'SASL Bind: EXTERNAL', 1038 'OTP': 'SASL Bind: OTP', 1039 'NTLM': 'SASL Bind: NTLM', 1040 'SCRAM-SHA-1': 'SASL Bind: SCRAM-SHA-1', 1041 'SCRAM-SHA-256': 'SASL Bind: SCRAM-SHA-256', 1042 } 1043 1044 def __init__( 1045 self, 1046 name='login_mech', 1047 text='Authentication mechanism', 1048 default=None, 1049 required=False, 1050 accesskey='', 1051 size=1, 1052 ): 1053 Select.__init__( 1054 self, 1055 name, text, maxValues=1, 1056 required=required, 1057 options=None, 1058 default=default or [], 1059 accesskey=accesskey, 1060 size=size, 1061 ignoreCase=0, 1062 multiSelect=0 1063 ) 1064 1065 def set_options(self, options): 1066 options_dict = {} 1067 options_dict[''] = self.supported_bind_mechs[''] 1068 for sasl_mech in options or self.supported_bind_mechs.keys(): 1069 sasl_mech = sasl_mech.upper() 1070 if sasl_mech in self.supported_bind_mechs: 1071 options_dict[sasl_mech] = self.supported_bind_mechs[sasl_mech] 1072 Select.set_options(self, options_dict.items()) 1073