1# 2# Extended object interface to entries in LDAP directories or LDIF files. 3# 4# (c) 2006-2018 Pierre David (pdav@users.sourceforge.net) 5# 6# $Id: ldapx.tcl,v 1.12 2008/02/07 21:19:39 pdav Exp $ 7# 8# History: 9# 2006/08/08 : pda : design 10# 11 12package require Tcl 8.4 13package require snit ;# tcllib 14package require uri 1.1.5 ;# tcllib 15package require base64 ;# tcllib 16package require ldap 1.6 ;# tcllib, low level code for LDAP directories 17 18package provide ldapx 1.1 19 20############################################################################## 21# LDAPENTRY object type 22############################################################################## 23 24snit::type ::ldapx::entry { 25 ######################################################################### 26 # Variables 27 ######################################################################### 28 29 # 30 # Format of an individual entry 31 # May be "standard" (standard LDAP entry, read from an LDAP directory 32 # or from a LDIF channel) or "change" (LDIF change, or result of the 33 # comparison of two standard entries). 34 # Special : "uninitialized" means that this entry has not been used, 35 # and the first use will initialize it. 36 # 37 38 variable format "uninitialized" 39 40 # 41 # DN 42 # 43 44 variable dn "" 45 46 # 47 # Standard entry 48 # 49 # Syntax: 50 # - array indexed by attribute names (lower case) 51 # - each value is the list of attributes 52 # 53 # The current state may be backed up in an internal state. 54 # (see backup and restore methods) 55 # 56 57 variable attrvals -array {} 58 59 variable backup 0 60 variable bckav -array {} 61 variable bckdn "" 62 63 # 64 # Change entry 65 # 66 # Syntax: 67 # {{<op> <parameters>} ... } 68 # if <op> = mod 69 # {mod {{<modop> <attr> [ {<val1> ... <valn>} ]} ...} } 70 # where <modop> = modrepl, modadd, moddel 71 # if <op> = add 72 # {add {<attr> {<val1> ... <valn>} ...}} 73 # if <op> = del 74 # {del} 75 # if <op> = modrdn 76 # {modrdn <newrdn> <deleteoldrdn> [ <newsuperior> ]} 77 # 78 79 variable change "" 80 81 ######################################################################### 82 # Generic methods (for both standard and change entries) 83 ######################################################################### 84 85 # Resets the entry to an empty state 86 87 method reset {} { 88 89 set format "uninitialized" 90 set dn "" 91 array unset attrvals 92 set backup 0 93 array unset bckav 94 set bckdn "" 95 set change "" 96 } 97 98 # Returns current format 99 100 method format {} { 101 102 return $format 103 } 104 105 # Checks if entry is compatible with a certain format 106 # errors out if not 107 108 method compatible {ref} { 109 110 if {$format eq "uninitialized"} then { 111 set format $ref 112 } elseif {$format ne $ref} then { 113 return -code error \ 114 "Invalid operation on format $format (should be $ref)" 115 } 116 } 117 118 # Get or set the current dn 119 120 method dn {{newdn {-}}} { 121 122 if {$newdn ne "-"} then { 123 set dn $newdn 124 } 125 return $dn 126 } 127 128 # Get the "superior" (LDAP slang word) part of current dn 129 130 method superior {} { 131 132 set pos [string first "," $dn] 133 if {$pos == -1} then { 134 set r "" 135 } else { 136 set r [string range $dn [expr {$pos+1}] end] 137 } 138 return $r 139 } 140 141 # Get the "rdn" part of current dn 142 143 method rdn {} { 144 145 set pos [string first "," $dn] 146 if {$pos == -1} then { 147 set r "" 148 } else { 149 set r [string range $dn 0 [expr {$pos-1}]] 150 } 151 return $r 152 } 153 154 # Get a printable form of the contents 155 156 method print {} { 157 158 set r "dn: $dn" 159 switch -- $format { 160 uninitialized { 161 # nothing 162 } 163 standard { 164 foreach a [lsort [array names attrvals]] { 165 append r "\n$a: $attrvals($a)" 166 } 167 } 168 change { 169 if {[llength $change]} then { 170 append r "\n$change" 171 } 172 } 173 default { 174 append r " (inconsistent value)" 175 } 176 } 177 return $r 178 } 179 180 # Prints the whole state of an entry 181 182 method debug {} { 183 184 set r "dn = <$dn>\nformat = $format" 185 switch -- $format { 186 uninitialized { 187 # nothing 188 } 189 standard { 190 foreach a [lsort [array names attrvals]] { 191 append r "\n\t$a: $attrvals($a)" 192 } 193 if {$backup} then { 194 append r "\nbackup dn = $bckdn" 195 foreach a [lsort [array names bckav]] { 196 append r "\n\t$a: $bckav($a)" 197 } 198 } else { 199 append r "\nno backup" 200 } 201 } 202 change { 203 if {[llength $change]} then { 204 append r "\n$change" 205 } else { 206 append r "\nno change" 207 } 208 } 209 default { 210 append r " (inconsistent value)" 211 } 212 } 213 return $r 214 } 215 216 217 ######################################################################### 218 # Methods for standard entries 219 ######################################################################### 220 221 # Tells if the current entry is empty 222 223 method isempty {} { 224 225 $self compatible "standard" 226 227 return [expr {[array size attrvals] == 0}] 228 } 229 230 # Get all values for an attribute 231 232 method get {attr} { 233 234 $self compatible "standard" 235 236 set a [string tolower $attr] 237 if {[info exists attrvals($a)]} then { 238 set r $attrvals($a) 239 } else { 240 set r {} 241 } 242 return $r 243 } 244 245 # Get only the first value for an attribute 246 247 method get1 {attr} { 248 249 return [lindex [$self get $attr] 0] 250 } 251 252 253 # Set all values for an attribute 254 255 method set {attr vals} { 256 257 $self compatible "standard" 258 259 set a [string tolower $attr] 260 if {[llength $vals]} then { 261 set attrvals($a) $vals 262 } else { 263 unset -nocomplain attrvals($a) 264 } 265 return $vals 266 } 267 268 # Set only one value for an attribute 269 270 method set1 {attr val} { 271 272 if {$val eq ""} then { 273 set l {} 274 } else { 275 set l [list $val] 276 } 277 278 return [$self set $attr $l] 279 } 280 281 # Add some values to an attribute 282 283 method add {attr vals} { 284 285 $self compatible "standard" 286 287 set a [string tolower $attr] 288 foreach v $vals { 289 lappend attrvals($a) $v 290 } 291 return $attrvals($a) 292 } 293 294 # Add only one value to an attribute 295 296 method add1 {attr val} { 297 298 return [$self add $attr [list $val]] 299 } 300 301 # Delete all values (or some values only) for an attribute 302 303 method del {attr {vals {}}} { 304 305 $self compatible "standard" 306 307 set a [string tolower $attr] 308 if {[llength $vals]} then { 309 set l [$self get $attr] 310 foreach v $vals { 311 while {[set pos [lsearch -exact $l $v]] != -1} { 312 set l [lreplace $l $pos $pos] 313 } 314 } 315 } else { 316 set l {} 317 } 318 319 if {[llength $l]} then { 320 $self set $attr $l 321 } else { 322 unset -nocomplain attrvals($a) 323 } 324 return 325 } 326 327 # Delete only one value from an attribute 328 329 method del1 {attr val} { 330 331 $self del $attr [list $val] 332 } 333 334 # Get all attribute names 335 336 method getattr {} { 337 338 $self compatible "standard" 339 340 return [array names attrvals] 341 } 342 343 # Get all attribute names and values 344 345 method getall {} { 346 347 $self compatible "standard" 348 349 return [array get attrvals] 350 } 351 352 # Reset all attribute names and values at once 353 354 method setall {lst} { 355 356 $self compatible "standard" 357 358 array unset attrvals 359 foreach {attr vals} $lst { 360 set a [string tolower $attr] 361 set attrvals($a) $vals 362 } 363 } 364 365 # Back up current entry into a new one or into the internal backup state 366 367 method backup {{other {}}} { 368 369 $self compatible "standard" 370 371 if {$other eq ""} then { 372 # 373 # Back-up entry in $self->$oldav and $self->$dn 374 # 375 set backup 1 376 set bckdn $dn 377 378 array unset bckav 379 array set bckav [array get attrvals] 380 } else { 381 # 382 # Back-up entry in $other 383 # 384 $other compatible "standard" 385 $other dn $dn 386 $other setall [array get attrvals] 387 } 388 } 389 390 # Restore current entry from an old one or from the internal backup state 391 392 method restore {{other {}}} { 393 394 $self compatible "standard" 395 396 if {$backup} then { 397 if {$other eq ""} then { 398 # 399 # Restore in current context 400 # 401 set dn $bckdn 402 array unset attrvals 403 array set attrvals [array get bckav] 404 } else { 405 # 406 # Restore in another object 407 # 408 $other compatible "standard" 409 $other dn $bckdn 410 $other setall [array get bckav] 411 } 412 } else { 413 return -code error \ 414 "Cannot restore a non backuped object" 415 } 416 } 417 418 # Swap current and backup data, if they reside in the same entry 419 420 method swap {} { 421 422 $self compatible "standard" 423 424 if {$backup} then { 425 # 426 # Swap current and backup contexts 427 # 428 set swdn $dn 429 set dn $bckdn 430 set bckdn $swdn 431 432 set swav [array get attrvals] 433 array unset attrvals 434 array set attrvals [array get bckav] 435 array unset bckav 436 array set bckav $swav 437 } else { 438 return -code error \ 439 "Cannot swap a non backuped object" 440 } 441 } 442 443 # Apply some modifications (given by a change entry) to current entry 444 445 method apply {chg} { 446 447 $self compatible "standard" 448 $chg compatible "change" 449 450 # 451 # Apply $chg modifications to $self 452 # 453 454 foreach mod [$chg change] { 455 set op [lindex $mod 0] 456 switch -- $op { 457 add { 458 if {! [$self isempty]} then { 459 return -code error \ 460 "Cannot add an entry to a non-empty entry" 461 } 462 $self setall [lindex $mod 1] 463 if {[string equal [$self dn] ""]} then { 464 $self dn [$chg dn] 465 } 466 } 467 mod { 468 foreach submod [lindex $mod 1] { 469 set subop [lindex $submod 0] 470 set attr [lindex $submod 1] 471 set vals [lindex $submod 2] 472 switch -- $subop { 473 modadd { 474 $self add $attr $vals 475 } 476 moddel { 477 $self del $attr $vals 478 } 479 modrepl { 480 $self del $attr 481 $self add $attr $vals 482 } 483 default { 484 return -code error \ 485 "Invalid submod operation '$subop'" 486 } 487 } 488 } 489 } 490 del { 491 array unset attrvals 492 } 493 modrdn { 494 set newrdn [lindex $mod 1] 495 set delold [lindex $mod 2] 496 set newsup [lindex $mod 3] 497 498 if {! [regexp {^([^=]+)=([^,]+)$} $newrdn m nattr nval]} then { 499 return -code "Invalid new RDN '$newrdn'" 500 } 501 502 set olddn [$self dn] 503 if {! [regexp {^([^=]+)=([^,]+),(.*)} $olddn m oattr oval osup]} then { 504 return -code "Invalid old DN '$olddn'" 505 } 506 507 if {$newsup eq ""} then { 508 set dn "$newrdn,$osup" 509 } else { 510 set dn "$newrdn,$newsup" 511 } 512 $self dn $dn 513 514 if {$delold} then { 515 $self del1 $oattr $oval 516 } 517 518 # XXX should we ignore case ? 519 if {[lsearch -exact [$self get $nattr] $nval] == -1} then { 520 $self add1 $nattr $nval 521 } 522 } 523 default { 524 return -code error \ 525 "Invalid change operation '$op'" 526 } 527 } 528 } 529 } 530 531 ######################################################################### 532 # Methods for change entries 533 ######################################################################### 534 535 # Get or set all modifications 536 537 method change {{newchg {-}}} { 538 539 $self compatible "change" 540 541 if {$newchg ne "-"} then { 542 set change $newchg 543 } 544 return $change 545 } 546 547 # Compute the difference between two entries (or between an entry 548 # and the backed-up internal state) into the current change entry 549 # e1 : new, e2 : old 550 # if e2 is not given, it defaults to backup in e1 551 552 method diff {new {old {}}} { 553 554 $self compatible "change" 555 556 # 557 # Select where backup is. If internal, creates a temporary 558 # standard entry. 559 # 560 561 if {$old eq ""} then { 562 set destroy_old 1 563 set old [::ldapx::entry create %AUTO%] 564 $new restore $old 565 } else { 566 set destroy_old 0 567 } 568 569 # 570 # Computes differences between values in the two entries 571 # 572 573 if {[$old dn] ne ""} then { 574 $self dn [$old dn] 575 } elseif {[$new dn] ne ""} then { 576 $self dn [$new dn] 577 } else { 578 $self dn "" 579 } 580 581 switch -- "[$new isempty][$old isempty]" { 582 00 { 583 # They may differ 584 set change [DiffEntries $new $old] 585 } 586 01 { 587 # new has been added 588 set change [list [list "add" [$new getall]]] 589 } 590 10 { 591 # new has been deleted 592 set change [list [list "del"]] 593 } 594 11 { 595 # they are both empty: no change 596 set change {} 597 } 598 } 599 600 # 601 # Remove temporary standard entry (backup was internal) 602 # 603 604 if {$destroy_old} then { 605 $old destroy 606 } 607 608 return $change 609 } 610 611 # local procedure to compute differences between two non empty entries 612 613 proc DiffEntries {new old} { 614 array set tnew [$new getall] 615 array set told [$old getall] 616 617 set lmod {} 618 619 # 620 # First step : is there a DN change? 621 # 622 623 set moddn [DiffDn [$new dn] [$old dn] tnew told] 624 625 # 626 # Second step : pick up changes in attributes and/or values 627 # 628 629 foreach a [array names tnew] { 630 if {[info exists told($a)]} then { 631 # 632 # They are new and old values for this attribute. 633 # We cannot use individual delete or add (rfc 4512, 634 # paragraph 2.5.1) for attributes which do not have an 635 # equality operator, so we use "replace" everywhere. 636 # 637 638 set lnew [lsort $tnew($a)] 639 set lold [lsort $told($a)] 640 if {$lold ne $lnew} then { 641 lappend lmod [list "modrepl" $a $tnew($a)] 642 } 643 644 unset tnew($a) 645 unset told($a) 646 } else { 647 lappend lmod [list "modadd" $a $tnew($a)] 648 unset tnew($a) 649 } 650 } 651 652 foreach a [array names told] { 653 lappend lmod [list "moddel" $a] 654 } 655 656 set lchg {} 657 658 if {[llength $lmod]} then { 659 lappend lchg [list "mod" $lmod] 660 } 661 662 # 663 # Third step : insert modDN changes 664 # 665 666 if {[llength $moddn]} then { 667 set newrdn [lindex $moddn 0] 668 set deleteoldrdn [lindex $moddn 1] 669 set newsuperior [lindex $moddn 2] 670 671 set lmod [list "modrdn" $newrdn $deleteoldrdn] 672 if {! [string equal $newsuperior ""]} then { 673 lappend lmod $newsuperior 674 } 675 lappend lchg $lmod 676 } 677 678 return $lchg 679 } 680 681 proc DiffDn {newdn olddn _tnew _told} { 682 upvar $_tnew tnew 683 upvar $_told told 684 685 # 686 # If DNs are the same, exit 687 # 688 689 if {[string equal -nocase $newdn $olddn]} then { 690 return {} 691 } 692 693 # 694 # Split components of both DNs : attribute, value, superior 695 # 696 697 if {! [regexp {^([^=]+)=([^,]+),(.*)} $olddn m oattr oval osup]} then { 698 return -code "Invalid old DN '$olddn'" 699 } 700 set oattr [string tolower $oattr] 701 set ordn "$oattr=$oval" 702 703 if {! [regexp {^([^=]+)=([^,]+),(.*)} $newdn m nattr nval nsup]} then { 704 return -code "Invalid new DN '$newdn'" 705 } 706 set nattr [string tolower $nattr] 707 set nrdn "$nattr=$nval" 708 709 # 710 # Checks if superior has changed 711 # 712 713 if {! [string equal -nocase $osup $nsup]} then { 714 set newsuperior $nsup 715 } else { 716 set newsuperior "" 717 } 718 719 # 720 # Checks if rdn has changed 721 # 722 723 if {! [string equal -nocase $ordn $nrdn]} then { 724 # 725 # Checks if old rdn must be deleted 726 # 727 728 set deleteoldrdn 1 729 if {[info exists tnew($oattr)]} then { 730 set pos [lsearch -exact [string tolower $tnew($oattr)] \ 731 [string tolower $oval]] 732 if {$pos != -1} then { 733 set deleteoldrdn 0 734 } 735 } 736 737 # 738 # Remove old and new rdn such as DiffEntries doesn't 739 # detect any modification. 740 # 741 742 foreach t {tnew told} { 743 foreach {a v} [list $oattr $oval $nattr $nval] { 744 if {[info exists ${t}($a)]} then { 745 set l [set ${t}($a)] 746 set pos [lsearch -exact [string tolower $l] \ 747 [string tolower $v] ] 748 if {$pos != -1} then { 749 set l [lreplace $l $pos $pos] 750 if {[llength $l]} then { 751 set ${t}($a) $l 752 } else { 753 unset -nocomplain ${t}($a) 754 } 755 } 756 } 757 } 758 } 759 } else { 760 set deleteoldrdn 0 761 } 762 763 return [list $nrdn $deleteoldrdn $newsuperior] 764 } 765 766 767 ######################################################################### 768 # End of ldapentry 769 ######################################################################### 770} 771 772############################################################################## 773# UTF8 translator, component used to manage the -utf8 option 774############################################################################## 775 776snit::type ::ldapx::utf8trans { 777 778 ######################################################################### 779 # Option 780 ######################################################################### 781 782 option -utf8 -default {{.*} {}} 783 784 ######################################################################### 785 # Methods 786 ######################################################################### 787 788 method must {attr} { 789 set utf8yes [lindex $options(-utf8) 0] 790 set utf8no [lindex $options(-utf8) 1] 791 set r 0 792 if {[regexp -expanded -nocase "^$utf8yes$" $attr]} then { 793 set r 1 794 if {[regexp -expanded -nocase "^$utf8no$" $attr]} then { 795 set r 0 796 } 797 } 798 return $r 799 } 800 801 method encode {attr val} { 802 if {[$self must $attr]} then { 803 set val [encoding convertto utf-8 $val] 804 } 805 return $val 806 } 807 808 method decode {attr val} { 809 if {[$self must $attr]} then { 810 set val [encoding convertfrom utf-8 $val] 811 } 812 return $val 813 } 814 815 method encodepairs {avpairs} { 816 set r {} 817 foreach {attr vals} $avpairs { 818 if {[llength $vals]} then { 819 lappend r $attr [$self encode $attr $vals] 820 } else { 821 lappend r $attr 822 } 823 } 824 return $r 825 } 826 827 method decodepairs {avpairs} { 828 set r {} 829 foreach {attr vals} $avpairs { 830 set vals [$self decode $attr $vals] 831 lappend r $attr $vals 832 } 833 return $r 834 } 835} 836 837############################################################################## 838# LDAP object type 839############################################################################## 840 841snit::type ::ldapx::ldap { 842 ######################################################################### 843 # Options 844 # 845 # note : options are lowercase 846 ######################################################################### 847 848 option -scope -default "sub" 849 option -derefaliases -default "never" 850 option -sizelimit -default 0 851 option -timelimit -default 0 852 option -attrsonly -default 0 853 854 component translator 855 delegate option -utf8 to translator 856 857 # 858 # Channel descriptor 859 # 860 861 variable channel "" 862 variable bind 0 863 864 # 865 # Last error 866 # 867 868 variable lastError "" 869 870 # 871 # Defaults connection modes 872 # 873 874 variable connect_defaults -array { 875 ldap {389 ::ldap::connect} 876 ldaps {636 ::ldap::secure_connect} 877 } 878 879 880 ######################################################################### 881 # Constructor 882 ######################################################################### 883 884 constructor {args} { 885 install translator using ::ldapx::utf8trans create %AUTO% 886 $self configurelist $args 887 } 888 889 destructor { 890 catch {$translator destroy} 891 } 892 893 ######################################################################### 894 # Methods 895 ######################################################################### 896 897 # Get or set the last error message 898 899 method error {{le {-}}} { 900 901 if {! [string equal $le "-"]} then { 902 set lastError $le 903 } 904 return $lastError 905 } 906 907 # Connect to the LDAP directory, and binds to it if needed 908 909 method connect {url {binddn {}} {bindpw {}}} { 910 911 array set comp [::uri::split $url "ldap"] 912 913 if {! [::info exists comp(host)]} then { 914 $self error "Invalid host in URL '$url'" 915 return 0 916 } 917 918 set scheme $comp(scheme) 919 if {! [::info exists connect_defaults($scheme)]} then { 920 $self error "Unrecognized URL '$url'" 921 return 0 922 } 923 924 set defport [lindex $connect_defaults($scheme) 0] 925 set fct [lindex $connect_defaults($scheme) 1] 926 927 if {[string equal $comp(port) ""]} then { 928 set comp(port) $defport 929 } 930 931 if {[Check $selfns {set channel [$fct $comp(host) $comp(port)]}]} then { 932 return 0 933 } 934 935 if {$binddn eq ""} then { 936 set bind 0 937 } else { 938 set bind 1 939 if {[Check $selfns {::ldap::bind $channel $binddn $bindpw}]} then { 940 return 0 941 } 942 } 943 return 1 944 } 945 946 # Disconnect from the LDAP directory 947 948 method disconnect {} { 949 950 Connected $selfns 951 952 if {$bind} { 953 if {[Check $selfns {::ldap::unbind $channel}]} then { 954 return 0 955 } 956 } 957 if {[Check $selfns {::ldap::disconnect $channel}]} then { 958 return 0 959 } 960 set channel "" 961 return 1 962 } 963 964 # New control structure : traverse the DIT and execute the body 965 # for each found entry. 966 967 method traverse {base filter attrs entry body} { 968 969 Connected $selfns 970 971 global errorInfo errorCode 972 973 set lastError "" 974 975 # 976 # Initiate search 977 # 978 979 set opt [list \ 980 -scope $options(-scope) \ 981 -derefaliases $options(-derefaliases) \ 982 -sizelimit $options(-sizelimit) \ 983 -timelimit $options(-timelimit) \ 984 -attrsonly $options(-attrsonly) \ 985 ] 986 987 ::ldap::searchInit $channel $base $filter $attrs $opt 988 989 # 990 # Execute the specific body for each result found 991 # 992 993 while {1} { 994 # 995 # The first call to searchNext may fail when searchInit 996 # is given some invalid parameters. 997 # We must terminate the current search in order to allow 998 # future searches. 999 # 1000 1001 set err [catch {::ldap::searchNext $channel} r] 1002 1003 if {$err} then { 1004 set ei $errorInfo 1005 set ec $errorCode 1006 ::ldap::searchEnd $channel 1007 return -code error -errorinfo $ei -errorcode $ec $r 1008 } 1009 1010 # 1011 # End of result messages 1012 # 1013 1014 if {[llength $r] == 0} then { 1015 break 1016 } 1017 1018 # 1019 # Set DN and attributes-values (converted from utf8 if needed) 1020 # for the entry 1021 # 1022 1023 $entry reset 1024 1025 $entry dn [lindex $r 0] 1026 $entry setall [$translator decodepairs [lindex $r 1]] 1027 1028 # 1029 # Execute body with the entry 1030 # 1031 # http://wiki.tcl.tk/685 1032 # 1033 1034 set code [catch {uplevel 1 $body} msg] 1035 switch -- $code { 1036 0 { 1037 # ok 1038 } 1039 1 { 1040 # error 1041 set ei $errorInfo 1042 set ec $errorCode 1043 ::ldap::searchEnd $channel 1044 return -code error -errorinfo $ei -errorcode $ec $msg 1045 } 1046 2 { 1047 # return 1048 ::ldap::searchEnd $channel 1049 return -code return $msg 1050 } 1051 3 { 1052 # break 1053 ::ldap::searchEnd $channel 1054 return {} 1055 } 1056 4 { 1057 # continue 1058 } 1059 default { 1060 # user defined 1061 ::ldap::searchEnd $channel 1062 return -code $code $msg 1063 } 1064 } 1065 } 1066 1067 # 1068 # Terminate search 1069 # 1070 1071 ::ldap::searchEnd $channel 1072 } 1073 1074 # Returns a list of newly created objects which match 1075 1076 method search {base filter attrs} { 1077 1078 Connected $selfns 1079 1080 set e [::ldapx::entry create %AUTO%] 1081 set r {} 1082 $self traverse $base $filter $attrs $e { 1083 set new [::ldapx::entry create %AUTO%] 1084 $e backup $new 1085 lappend r $new 1086 } 1087 $e destroy 1088 return $r 1089 } 1090 1091 # Read one or more entries, and returns the number of entries found. 1092 # Useful to easily read one or more entries. 1093 1094 method read {base filter args} { 1095 1096 set n 0 1097 set max [llength $args] 1098 set e [::ldapx::entry create %AUTO%] 1099 $self traverse $base $filter {} $e { 1100 if {$n < $max} then { 1101 $e backup [lindex $args $n] 1102 } 1103 incr n 1104 } 1105 return $n 1106 } 1107 1108 # Commit a list of changes (or standard, backuped entries) 1109 1110 method commit {args} { 1111 1112 Connected $selfns 1113 1114 foreach entry $args { 1115 switch -- [$entry format] { 1116 uninitialized { 1117 return -code error \ 1118 "Uninitialized entry" 1119 } 1120 standard { 1121 set echg [::ldapx::entry create %AUTO%] 1122 set lchg [$echg diff $entry] 1123 set dn [$echg dn] 1124 $echg destroy 1125 } 1126 change { 1127 set dn [$entry dn] 1128 set lchg [$entry change] 1129 } 1130 } 1131 1132 foreach chg $lchg { 1133 set op [lindex $chg 0] 1134 1135 switch -- $op { 1136 {} { 1137 # nothing to do 1138 } 1139 add { 1140 set av [$translator encodepairs [lindex $chg 1]] 1141 if {[Check $selfns {::ldap::addMulti $channel $dn $av}]} then { 1142 return 0 1143 } 1144 } 1145 del { 1146 if {[Check $selfns {::ldap::delete $channel $dn}]} then { 1147 return 0 1148 } 1149 } 1150 mod { 1151 set lrep {} 1152 set ldel {} 1153 set ladd {} 1154 1155 foreach submod [lindex $chg 1] { 1156 set subop [lindex $submod 0] 1157 set attr [lindex $submod 1] 1158 set vals [lindex $submod 2] 1159 1160 set vals [$translator encode $attr $vals] 1161 switch -- $subop { 1162 modadd { 1163 lappend ladd $attr $vals 1164 } 1165 moddel { 1166 lappend ldel $attr $vals 1167 } 1168 modrepl { 1169 lappend lrep $attr $vals 1170 } 1171 } 1172 } 1173 1174 if {[Check $selfns {::ldap::modifyMulti $channel $dn \ 1175 $lrep $ldel $ladd}]} then { 1176 return 0 1177 } 1178 } 1179 modrdn { 1180 set newrdn [lindex $chg 1] 1181 set delOld [lindex $chg 2] 1182 set newSup [lindex $chg 3] 1183 if {[string equal $newSup ""]} then { 1184 if {[Check $selfns {::ldap::modifyDN $channel $dn \ 1185 $newrdn $delOld}]} then { 1186 return 0 1187 } 1188 } else { 1189 if {[Check $selfns {::ldap::modifyDN $channel $dn \ 1190 $newrdn $delOld $newSup}]} then { 1191 return 0 1192 } 1193 } 1194 } 1195 } 1196 } 1197 } 1198 1199 return 1 1200 } 1201 1202 ######################################################################### 1203 # Local procedures 1204 ######################################################################### 1205 1206 proc Connected {selfns} { 1207 if {$channel eq ""} then { 1208 return -code error \ 1209 "Object not connected" 1210 } 1211 } 1212 1213 proc Check {selfns script} { 1214 return [catch {uplevel 1 $script} lastError] 1215 } 1216 1217 ######################################################################### 1218 # End of LDAP object type 1219 ######################################################################### 1220} 1221 1222############################################################################## 1223# LDIF object type 1224############################################################################## 1225 1226snit::type ::ldapx::ldif { 1227 1228 ######################################################################### 1229 # Options 1230 ######################################################################### 1231 1232 # 1233 # Fields to ignore when reading change file 1234 # 1235 1236 option -ignore {} 1237 1238 component translator 1239 delegate option -utf8 to translator 1240 1241 1242 ######################################################################### 1243 # Variables 1244 ######################################################################### 1245 1246 # 1247 # Version of LDIF file (0 means : uninitialized) 1248 # 1249 1250 variable version 0 1251 1252 # 1253 # Channel descriptor 1254 # 1255 1256 variable channel "" 1257 1258 # 1259 # Line number 1260 # 1261 1262 variable lineno 0 1263 1264 # 1265 # Last error message 1266 # 1267 1268 variable lastError "" 1269 1270 # 1271 # Number of entries read or written 1272 # 1273 1274 variable nentries 0 1275 1276 # 1277 # Type of LDIF file 1278 # 1279 1280 variable format "uninitialized" 1281 1282 ######################################################################### 1283 # Constructor 1284 ######################################################################### 1285 1286 constructor {args} { 1287 install translator using ::ldapx::utf8trans create %AUTO% 1288 $self configurelist $args 1289 } 1290 1291 destructor { 1292 catch {$translator destroy} 1293 } 1294 1295 ######################################################################### 1296 # Methods 1297 ######################################################################### 1298 1299 # Initialize a channel 1300 1301 method channel {newchan} { 1302 1303 set channel $newchan 1304 set version 0 1305 set nentries 0 1306 set format "uninitialized" 1307 set lineno 0 1308 return 1309 } 1310 1311 # Get or set the last error message 1312 1313 method error {{le {-}}} { 1314 1315 if {$le ne "-"} then { 1316 set lastError $le 1317 } 1318 return $lastError 1319 } 1320 1321 # An LDIF file cannot include both changes and standard entries 1322 # (see RFC 2849, page 2). Check this. 1323 1324 method compatible {ref} { 1325 1326 if {$format eq "uninitialized"} then { 1327 set format $ref 1328 } elseif {$format ne $ref} then { 1329 return -code error \ 1330 "Invalid entry ($ref) type for LDIF $format file" 1331 } 1332 } 1333 1334 # Reads an LDIF entry (standard or change) from the channel 1335 # returns 1 if ok, 0 if error or EOF 1336 1337 # XXX this method is just coded for tests at this time 1338 1339 method debugread {entry} { 1340 1341 $entry compatible "standard" 1342 $entry dn "uid=joe,ou=org,o=com" 1343 $entry setall {uid {joe} sn {User} givenName {Joe} cn {{Joe User}} 1344 telephoneNumber {+31415926535 +27182818285} objectClass {person} 1345 } 1346 return 1 1347 } 1348 1349 # Read an LDIF entry (standard or change) from the channel 1350 # returns 1 if ok, 0 if error or EOF 1351 1352 method read {entry} { 1353 if {$channel eq ""} then { 1354 return -code error \ 1355 "Channel not initialized" 1356 } 1357 1358 set r [Lexical $selfns] 1359 if {[lindex $r 0] ne "err"} then { 1360 set r [Syntaxic $selfns [lindex $r 1]] 1361 } 1362 1363 if {[lindex $r 0] eq "err"} then { 1364 set lastError [lindex $r 1] 1365 return 0 1366 } 1367 1368 switch -- [lindex $r 0] { 1369 uninitialized { 1370 $entry reset 1371 set lastError "" 1372 set r 0 1373 } 1374 standard { 1375 if {[catch {$self compatible "change"}]} then { 1376 set lastError "Standard entry not allowed in LDIF change file" 1377 set r 0 1378 } else { 1379 $entry reset 1380 $entry dn [lindex $r 1] 1381 $entry setall [lindex $r 2] 1382 set r 1 1383 } 1384 } 1385 change { 1386 if {[catch {$self compatible "change"}]} then { 1387 set lastError "Change entry not allowed in LDIF standard file" 1388 set r 0 1389 } else { 1390 $entry reset 1391 $entry dn [lindex $r 1] 1392 $entry change [list [lindex $r 2]] 1393 set r 1 1394 } 1395 } 1396 default { 1397 return -code error \ 1398 "Internal error (invalid returned entry format)" 1399 } 1400 } 1401 1402 return $r 1403 } 1404 1405 # Write an LDIF entry to the channel 1406 1407 method write {entry} { 1408 1409 if {$channel eq ""} then { 1410 return -code error \ 1411 "Channel not initialized" 1412 } 1413 1414 switch -- [$entry format] { 1415 uninitialized { 1416 # nothing 1417 } 1418 standard { 1419 if {[llength [$entry getall]]} then { 1420 $self compatible "standard" 1421 1422 if {$nentries == 0} then { 1423 if {$version == 0} then { 1424 set version 1 1425 } 1426 WriteLine $selfns "version" "$version" 1427 puts $channel "" 1428 } 1429 1430 WriteLine $selfns "dn" [$entry dn] 1431 1432 foreach a [$entry getattr] { 1433 foreach v [$entry get $a] { 1434 WriteLine $selfns $a $v 1435 } 1436 } 1437 puts $channel "" 1438 } 1439 } 1440 change { 1441 $self compatible "change" 1442 1443 set lchg [$entry change] 1444 foreach chg $lchg { 1445 if {$nentries == 0} then { 1446 if {$version == 0} then { 1447 set version 1 1448 } 1449 WriteLine $selfns "version" "$version" 1450 puts $channel "" 1451 } 1452 1453 WriteLine $selfns "dn" [$entry dn] 1454 1455 set op [lindex $chg 0] 1456 switch -- $op { 1457 add { 1458 WriteLine $selfns "changetype" "add" 1459 foreach {attr vals} [lindex $chg 1] { 1460 foreach v $vals { 1461 WriteLine $selfns $attr $v 1462 } 1463 } 1464 } 1465 del { 1466 WriteLine $selfns "changetype" "delete" 1467 } 1468 mod { 1469 WriteLine $selfns "changetype" "modify" 1470 foreach submod [lindex $chg 1] { 1471 set subop [lindex $submod 0] 1472 set attr [lindex $submod 1] 1473 set vals [lindex $submod 2] 1474 1475 switch -- $subop { 1476 modadd { 1477 WriteLine $selfns "add" $attr 1478 } 1479 moddel { 1480 WriteLine $selfns "delete" $attr 1481 } 1482 modrepl { 1483 WriteLine $selfns "replace" $attr 1484 } 1485 } 1486 foreach v $vals { 1487 WriteLine $selfns $attr $v 1488 } 1489 puts $channel "-" 1490 } 1491 } 1492 modrdn { 1493 WriteLine $selfns "changetype" "modrdn" 1494 set newrdn [lindex $chg 1] 1495 set delold [lindex $chg 2] 1496 set newsup [lindex $chg 3] 1497 WriteLine $selfns "newrdn" $newrdn 1498 WriteLine $selfns "deleteOldRDN" $delold 1499 if {$newsup ne ""} then { 1500 WriteLine $selfns "newSuperior" $newsup 1501 } 1502 } 1503 } 1504 puts $channel "" 1505 incr nentries 1506 } 1507 } 1508 default { 1509 return -code error \ 1510 "Invalid entry format" 1511 } 1512 } 1513 return 1 1514 } 1515 1516 ######################################################################### 1517 # Local procedures to read an entry 1518 ######################################################################### 1519 1520 # 1521 # Lexical analysis of an entry 1522 # Special case for "version:" entry. 1523 # Returns a list of lines {ok {{<attr1> <val1>} {<attr2> <val2>} ...}} 1524 # or a list {err <message>} 1525 # 1526 1527 proc Lexical {selfns} { 1528 set result {} 1529 set prev "" 1530 1531 while {[gets $channel line] > -1} { 1532 incr lineno 1533 1534 if {$line eq ""} then { 1535 # 1536 # Empty line: we are either before the beginning 1537 # of the entry or at the empty line after the 1538 # entry. 1539 # We don't give up before getting something. 1540 # 1541 1542 if {! [FlushLine $selfns "" result prev msg]} then { 1543 return [list "err" $msg] 1544 } 1545 1546 if {[llength $result]} then { 1547 break 1548 } 1549 1550 } elseif {[regexp {^[ \t]} $line]} then { 1551 # 1552 # Continuation line. Remove the continuation character. 1553 # 1554 1555 append prev [string range $line 1 end] 1556 1557 } elseif {[regexp {^-$} $line]} then { 1558 # 1559 # Separation between individual modifications 1560 # 1561 1562 if {! [FlushLine $selfns "" result prev msg]} then { 1563 return [list "err" $msg] 1564 } 1565 lappend result [list "-" {}] 1566 1567 } else { 1568 # 1569 # Should be a normal line (key: val) 1570 # 1571 1572 if {! [FlushLine $selfns $line result prev msg]} then { 1573 return [list "err" $msg] 1574 } 1575 1576 } 1577 } 1578 1579 # 1580 # End of file, or end of entry. Flush buffered data from $prev 1581 # for EOF case. 1582 # 1583 1584 if {! [FlushLine $selfns "" result prev msg]} then { 1585 return [list "err" $msg] 1586 } 1587 1588 return [list "ok" $result] 1589 } 1590 1591 proc FlushLine {selfns line _result _prev _msg} { 1592 upvar $_result result $_prev prev $_msg msg 1593 1594 if {$prev ne ""} then { 1595 set r [DecodeLine $selfns $prev] 1596 if {[llength $r] != 2} then { 1597 set msg "$lineno: invalid syntax" 1598 return 0 1599 } 1600 1601 # 1602 # Special case for "version: 1". This code should not 1603 # be in lexical analysis, but this would be too disruptive 1604 # in syntaxic analysis 1605 # 1606 1607 if {[string equal -nocase [lindex $r 0] "version"]} then { 1608 if {$version != 0} then { 1609 set msg "version attribute allowed only at the beginning of the LDIF file" 1610 return 0 1611 } 1612 set val [lindex $r 1] 1613 if {[catch {set val [expr {$val+0}]}]} then { 1614 set msg "invalid version value" 1615 return 0 1616 } 1617 if {$val != 1} then { 1618 set msg "unrecognized version '$val'" 1619 return 0 1620 } 1621 set version 1 1622 } else { 1623 lappend result $r 1624 } 1625 } 1626 set prev $line 1627 1628 return 1 1629 } 1630 1631 proc DecodeLine {selfns str} { 1632 if {[regexp {^([^:]*)::[ \t]*(.*)} $str d key val]} then { 1633 set key [string tolower $key] 1634 set val [::base64::decode $val] 1635 set val [$translator decode $key $val] 1636 set r [list $key $val] 1637 } elseif {[regexp {^([^:]*):[ \t]*(.*)} $str d key val]} then { 1638 set key [string tolower $key] 1639 set val [$translator decode $key $val] 1640 set r [list $key $val] 1641 } else { 1642 # syntax error 1643 set r {} 1644 } 1645 return $r 1646 } 1647 1648 # 1649 # Array indexed by current state of the LDIF automaton 1650 # Each element is a list of actions, each with the format: 1651 # pattern on on "attribute:value" 1652 # next state 1653 # script (to be evaled in Syntaxic local procedure) 1654 # 1655 1656 variable ldifautomaton -array { 1657 begin { 1658 {dn:* dn {set dn $val}} 1659 {EOF:* end {set r [list "empty"]}} 1660 } 1661 dn { 1662 {changetype:modify mod {set t "change" ; set r {mod}}} 1663 {changetype:modrdn modrdn {set t "change" ; set newsup {}}} 1664 {changetype:add add {set t "change"}} 1665 {changetype:delete del {set t "change"}} 1666 {*:* standard {set t "standard" ; lappend tab($key) $val}} 1667 } 1668 standard { 1669 {EOF:* end {set r [array get tab]}} 1670 {*:* standard {lappend tab($key) $val}} 1671 } 1672 mod { 1673 {add:* mod-add {set attr [string tolower $val] ; set vals {}}} 1674 {delete:* mod-del {set attr [string tolower $val] ; set vals {}}} 1675 {replace:* mod-repl {set attr [string tolower $val] ; set vals {}}} 1676 {EOF:* end {}} 1677 } 1678 mod-add { 1679 {*:* mod-add-attr {lappend vals $val}} 1680 } 1681 mod-add-attr { 1682 {-:* mod {lappend r [list "modadd" $attr $vals]}} 1683 {*:* mod-add-attr {lappend vals $val}} 1684 } 1685 mod-del { 1686 {-:* mod {lappend r [list "moddel" $attr $vals]}} 1687 {*:* mod-del {lappend vals $val}} 1688 } 1689 mod-repl { 1690 {-:* mod {lappend r [list "modrepl" $attr $vals]}} 1691 {*:* mod-repl {lappend vals $val}} 1692 } 1693 modrdn { 1694 {newrdn:* modrdn-new {set newrdn $val}} 1695 } 1696 modrdn-new { 1697 {deleteoldrdn:0 modrdn-del {set delold 0}} 1698 {deleteoldrdn:1 modrdn-del {set delold 1}} 1699 } 1700 modrdn-del { 1701 {newsuperior:* modrdn-end {set newsup $val}} 1702 {EOF:* end {set r [list modrdn $newrdn $delold] }} 1703 } 1704 modrdn-end { 1705 {EOF:* end {set r [list modrdn $newrdn $delold $newsup]}} 1706 } 1707 add { 1708 {EOF:* end {set r [list add [array get tab]]}} 1709 {*:* add {lappend tab($key) $val}} 1710 } 1711 del { 1712 {EOF:* end {set r [list del]}} 1713 } 1714 } 1715 1716 proc Syntaxic {selfns lcouples} { 1717 set state "begin" 1718 set newsup {} 1719 set t "uninitialized" 1720 foreach c $lcouples { 1721 set key [lindex $c 0] 1722 if {[lsearch [string tolower $options(-ignore)] $key] == -1} then { 1723 set val [lindex $c 1] 1724 set a [Automaton $selfns $state $key $val] 1725 if {$a eq ""} then { 1726 return [list "err" "Syntax error before line $lineno"] 1727 } 1728 set state [lindex $a 0] 1729 set script [lindex $a 1] 1730 eval $script 1731 } 1732 } 1733 1734 set a [Automaton $selfns $state "EOF" "EOF"] 1735 if {$a eq ""} then { 1736 return [list "err" "Premature EOF"] 1737 } 1738 set script [lindex $a 1] 1739 eval $script 1740 1741 set result [list $t] 1742 switch $t { 1743 uninitialized { 1744 # nothing 1745 } 1746 standard { 1747 lappend result $dn $r 1748 } 1749 change { 1750 lappend result $dn $r 1751 } 1752 } 1753 1754 return $result 1755 } 1756 1757 proc Automaton {selfns state key val} { 1758 set r {} 1759 if {[info exists ldifautomaton($state)]} then { 1760 foreach a $ldifautomaton($state) { 1761 if {[string match [lindex $a 0] "$key:$val"]} then { 1762 set r [lreplace $a 0 0] 1763 break 1764 } 1765 } 1766 } 1767 return $r 1768 } 1769 1770 ######################################################################### 1771 # Local procedures to write an entry 1772 ######################################################################### 1773 1774 proc WriteLine {selfns attr val} { 1775 1776 if {[string is ascii $val] && [string is print $val]} then { 1777 set sep ":" 1778 } else { 1779 set sep "::" 1780 set val [$translator encode $attr $val] 1781 set val [::base64::encode $val] 1782 } 1783 1784 set first 1 1785 foreach line [split $val "\n"] { 1786 if {$first} then { 1787 puts $channel "$attr$sep $line" 1788 set first 0 1789 } else { 1790 puts $channel " $line" 1791 } 1792 } 1793 } 1794} 1795