-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathViper.pm
2926 lines (2361 loc) · 92.1 KB
/
Viper.pm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package Viper;
# vim: se ts=2 sts=2 sw=2 ai
#
# Viper -- Custom Perl backend for use with the OpenLDAP server.
#
# Davor Ocelic <[email protected]>
# Crystal Labs, https://crystallabs.io/
# Released under GPL v3.
#
# The Viper backend implements regular LDAP functionality and can be used in
# general-purpose LDAP scenarios where you want quick results on a platform
# that comes with extra features (default entries, query rewriting, dynamic
# values, etc.), and also lends itself to further customization.
#
# Smallest working configuration can be seen in etc/ldap/slapd.conf.
#
use strict;
use warnings;
use IO::File qw//;
use Data::Dumper qw/Dumper/;
use File::Find::Rule qw/find/;
use Net::LDAP::Constant qw/LDAP_SUCCESS LDAP_PARAM_ERROR LDAP_OPERATIONS_ERROR/;
use Net::LDAP::Constant qw/LDAP_ALREADY_EXISTS LDAP_NO_SUCH_OBJECT LDAP_OTHER/;
use Net::LDAP::Constant qw/LDAP_INVALID_SYNTAX LDAP_INVALID_DN_SYNTAX/;
use Net::LDAP::Constant qw/LDAP_NOT_ALLOWED_ON_NONLEAF LDAP_FILTER_ERROR/;
use Net::LDAP::Constant qw/LDAP_INVALID_CREDENTIALS LDAP_UNWILLING_TO_PERFORM/;
use Net::LDAP::Constant qw/LDAP_TIMELIMIT_EXCEEDED LDAP_SIZELIMIT_EXCEEDED/;
use Net::LDAP::LDIF qw//;
use Net::LDAP::Schema qw//;
use Net::LDAP::Filter qw//;
use Net::LDAP::FilterMatch qw//;
use Storable qw/freeze nstore retrieve/;
use File::Path qw/rmtree mkpath/;
use Fcntl qw/:flock/;
use Net::LDAP qw//;
use Text::CSV_XS qw//;
use Memoize::Expire qw//;
use List::MoreUtils qw/any firstidx/;
use subs qw/p pd pc pcd po pod/;
# To make use of DEBUG, server should best run in foreground mode. Something like:
# sudo -u openldap /usr/sbin/slapd -d 0
use constant DEBUG => 1; # General debug?
use constant DEBUG_DTL=> 0; # Detailed debug?
use constant DEBUG_OVL=> 1; # Overlays debug?
use constant DEBUG_CCH=> 0; # Cache debug?
use constant CFG_STACK=> 1; # Allow save/reset/load config file routines
use constant CFG_DUMP => 1; # Allow savedump/loaddump config file routines
# Enable/disable options
use constant APPENDER => 1; # Enable appending with other entries' attributes.
use constant FILEVAL => 1; # Enable value expansion by reading files.
use constant EXPANDVAL=> 1; # Enable value expansion by loading DN attrs.
use constant FINDVAL => 1; # Enable re-searching and returning certain attr.
use constant PERLEVAL => 0; # Enable Perl evaluation of values.
use constant RELOCATOR=> 0; # Enable relocation of Debconf keys from client.
use constant PROMPTER => 0; # Enable relocation of Debconf keys from server.
use constant CACHE => 1; # Enable honoring cache parms for overlay values?
# Search scope defines
use constant BASE => 0;
use constant ONE => 1;
use constant SUB => 2;
use constant CHILDREN => 3;
our %SCOPE2NAME = (
0 => "base",
1 => "one",
2 => "sub",
3 => "children",
);
# Referral chasing
use constant NEVER => 0;
use constant ALWAYS => 1;
use constant SEARCH => 2;
use constant FIND => 3;
our %DEREF2NAME = (
0 => "never",
1 => "always",
2 => "search",
3 => "find",
);
BEGIN {
# No need to initialize the whole Debconf block if prompter is off.
# (This allows server to run on non-debian machine as long as you do
# not require server-side/debconf prompting).
return if !PROMPTER;
$ENV{DEBCONF_SYSTEMRC}= '/etc/debconf.conf.pipe';
require 'Debconf/Db.pm';
require 'Debconf/Config.pm';
require 'Debconf/AutoSelect.pm';
require 'Debconf/Format/822.pm';
require 'Debconf/DbDriver/LDAP.pm';
Debconf::DbDriver::LDAP->import( qw/parse_records/);
}
# Raw/binary value regex
our $RAW = qr/(?i:^jpegPhoto|;binary)/;
# LDAP scope to fs tree depth level
our %S2L= (
1 => 1, # ONE (1 level)
2 => undef, # SUB (unlimited)
3 => undef, # CHILDREN (unlimited)
);
# Overlays that will run on an entry if individual
# overlay is enabled. Name here should match the name of overlay's
# config array (see sub new()'s $this object) and in turn it is also
# name by which overlay will be recognized by run_overlays() within
# attribute's values.
our @OVERLAYS= ( grep{ defined} (
FILEVAL ? 'file' : undef,
EXPANDVAL ? 'exp' : undef,
FINDVAL ? 'find' : undef,
PERLEVAL ? 'perl' : undef,
));
# Necessary variables for Debconf interaction and server-side prompting.
# cin/cout, tin/tout: config DB in/out pipe, templates DB in/out pipe, used
# for initializing prompter's debconf state.
# dc/dt/df/dm: references to debconf's config/templates/frontend/confmodule.
our ( $cin, $cout, $tin, $tout);
our ( $dc, $dt, $df, $dm);
# Backend instance
sub new {
my( $class)= @_;
p "NEW @_ ----------------------------------------";
# Assign instance defaults. List all allowed options here,
# even if their value is '' or 0. (Do not use undef if you expect to
# set it from the config file, because it will implicitly make the
# option invalid).
# Scalars get values assigned, arrays pushed, hashes arrayref'd.
my $this= {
treesuffix => q{}, # Suffix (too bad we're not called with it),
# and 'suffix' directive can only be specified
# before Viper.pm module so we can't get that
# one from slapd. We basically have to invent
# a new directive and set it to the same value.
directory => q{}, # Base directory / datadir for tree. Can be
# different for each suffix, but suffixes when
# in the same directory can use each other's
# fallback/default values etc.
extension => '.ldif', # Extension for leaf nodes (files). Can be set
# to anything, but .ldif is usually most
# reasonable. Note that you cannot go without
# extension as that makes files and directories
# indistinguishable, and breaks the server.
addoverwrites => 0, # Allow ADD to overwrite existing entries?
addignoredups => 0, # If overw=0, ignore ADD on existing entries?
addrelocate => {}, # name=>[$a,$b] for $name==loc && $dn=~ s/$a/$b/
addprompt => {}, # Run prompter under name, cfg->tpl=~ s/$a/$b/
modifysmarts => 1, # Allow MODIFY to detect no-change
modifycopyonwrite=> 1, # Modify & copy dfl entry to new DN if !exist?
deletetrees => 0, # DELETE allows deleting of non-leafs?
searchsubst => [], # List of [...->...] search subst rules
searchfallback => [], # List of [$a,$b] for $dn or $dn=~ s/$a/$b/
entryappend => [], # List of [$a,$t,$b,$p,$n] to append with attrs
# All directives below are in form [ [$m, $nm], ... ], where $m and
# $nm are regexes that attribute name must match and NOT match (respe-
# ctively) for the overlay to execute on its values.
perl => [], # Match/No-Match regex list for perleval
'exp' => [], # Match/No-Match regex list for expandval
file => [], # Match/No-Match regex list for fileval
'find' => [], # Match/No-Match regex list for findval
overlayconfig => {}, # default overlay opts ('OVLNAME|default SPEC')
schemaldif => [], # Schema in LDIF format (to be aware of schema).
# To produce schema file, start server, then use
# schema.pl to retrieve schema from server in
# LDIF format and dump it to a file.
schemafatal => 0, # Missing schema is fatal problem? (Allowed to be
# missing by default so that you can run server,
# get schema, save it and have it on next start)
enablebind => 0, # Allow bind? Our implementation is minimal.
# Keys that control parsing behavior in slapd.conf and basically take
# effect directly when encountered.
message => q{}, # Print message to console
var => {}, # Define a variable, var NAME "V A L"
parse => 1, # Parse vars in following cfg lines (yes/no)
clean => q{}, # Clean temp/stack files in $directory/tmp/
save => q{}, # Save current cfg stack to named file
'reset' => q{}, # Clean cfg stack currently in memory
load => q{}, # Load cfg stack from a named file
'savedump' => q{}, # Storable dump to named file (for standalone)
'loaddump' => q{}, # Storable load from named file (for standalone)
# Keys that will contain data generated on startup or during run
schema => undef, # Will contain schema (load with 'schemaLDIF')
tmpdir => undef, # $directory/tmp, hardcoded
stack => [], # Current config stack, can 'save' or 'reset' it
standard_parse => undef, # Text::CSV_XS obj. for parsing various input
level => 0, # Loop/depth count. Usually 1
cache => {}, # Cache for dn2leaf file reads & ldif parsing
op_cache_valid => {}, # Num-ops cache validity (per-overlay)
cacheread => '', # Num-ops cache validity (dn2leaf disk reads)
'start' => [], # Time of search start (array ID= search level)
};
# Must be done here as schema parser already has to be present
# during config parsing step.
$this->{schema}= Net::LDAP::Schema->new;
bless $this, $class
}
# Called after all configuration processing is over
sub init {
my( $this)= @_;
p "INIT @_ $this->{treesuffix}";
# Let's do some checking.
# Make sure 'extension' is non-empty. (As to why, read note on
# 'extension' above).
if( not $this->{extension}) {
warn 'File extension for leaf nodes cannot be empty; ' .
"set a value (such as '.ldif') and restart slapd.\n";
return LDAP_PARAM_ERROR
}
# Now ensure that the root of the configured tree exists. To do that,
# all we need to do is create the components on the way to final
# component.
my $dn= $this->{treesuffix};
$dn=~ s/^.+?,\s*//; # Reduce DN to part of the path that needs to be there
my( $ret, %ret);
# Note: this return doesn't bail out on File not found because dn2leaf
# is called with namesonly param, so no actual checking is done.
$ret= $this->dn2leaf( "$dn", \%ret, qw/namesonly 1 leaf 0 writeop 1 create 1/);
return $ret unless $ret== LDAP_SUCCESS;
# Note: $dn=~ check is here to avoid the error message on
# suffixes with a single component (such as ou=defaults)
if( $dn=~ qr/,/o and not -e $ret{file}) {
warn 'Components leading up to the tree suffix '.
"for $this->{treesuffix} are missing; ".
"create them and restart slapd.\n";
return LDAP_OPERATIONS_ERROR
}
## Now make symlinks for all virtual paths, unless present already.
#my( $ret, %ret);
## Figure out our basedir where symlinks are to be created
## (dc=tmp is added here brutally like this so that $ret{directory} shows
## the directory we want, not one level too low).
#$ret= $this->dn2leaf( "dc=tmp,$this->{treesuffix}", \%ret, qw/namesonly 1/);
#return $ret unless $ret== LDAP_SUCCESS;
## XXX Error ckin
#mkpath $ret{directory};
#for my $virtual_line( @{ $this->{virtual}}) {
# for my $virtual( @$virtual_line) {
# $ret= $this->dn2leaf( "dc=tmp,$virtual", \%ret, qw/namesonly 1/);
# return $ret unless $ret== LDAP_SUCCESS;
# warn " VIRTUAL $virtual, PATH $ret{directory} / $ret{file}\n";
# }
#}
# Initialize schema obj. We do that in new(), but add ||= check here
# for situations where Viper is ran standalone using config dump from
# file, where references to objects are of course lost.
$this->{schema}||= new Net::LDAP::Schema;
# Debconf-related init code
$this->debconf_initialize if PROMPTER;
# Initialize Text::CSV_XS parser suitable for generic use
$this->{standard_parse}= Text::CSV_XS->new({
# We better use default escape_char and not \ because it's VERY
# confusing. (It's already too confusing that you DO have to write
# \\ instead of just \ in slapd.conf, but you DO NOT do that when
# a value is in LDIF source). So let's not add another layer of
# confusion -- the default escape char (" - literal double quote)
# probably works fine.
#escape_char => q{\\},
sep_char => q{ },
binary => 1,
# Unfortunately, this option does not result in us being able to use
# multiple spaces (probably due to sep_char also being whitespace).
#allow_whitespace => 1,
});
LDAP_SUCCESS
}
# Called to verify bind credentials.
#
# Note that if rootdn and rootpw are set in slapd.conf, and you try to
# authenticate as rootdn, slapd will perform the password check itself,
# and will not call this function.
#
# Also, if you authenticate as a user belonging to some other part of DIT,
# that backend's bind() will be called, not this one.
#
# Also, if you authenticate via e.g. GSSAPI, this won't be called.
#
# Finally, due to limited implementation of this function, we default to
# simply not allowing it, unless allow_bind is enabled.
sub bind {
my( $this, $dn, $pw)= @_;
return LDAP_UNWILLING_TO_PERFORM unless $this->{enablebind};
$this->normalize( \$dn);
p "BIND $dn"; # Print bind DN only, don't show $pw in log.
my ( $ret, undef, undef, $entry)= $this->load( $dn);
return LDAP_INVALID_CREDENTIALS unless $ret== LDAP_SUCCESS;
my @pws= $entry->get_value( 'userPassword');
# See if any userPassword (can be multi-value) matches
# the provided password
if( any{ "$pw" eq "$_"} @pws) {
return LDAP_SUCCESS
}
LDAP_INVALID_CREDENTIALS
}
# Handle our config lines. Called by slapd for each directive.
sub config {
my( $this, $key, @val)= @_;
$key= lc $key;
p "CONFIG $key @val";
# Support config file to specify longer names of config options for
# clarity. Internally, we use short names. I.e. a config line of
# "expandval opt1 opt2" is translated to "exp" internally.
# (But you can also specify 'exp' directly (or anything in between)).
if( !defined $this->{$key}) {
my @keys;
for my $cfgkey( keys %$this) {
push @keys, $cfgkey if $key=~ /^$cfgkey/
}
if( @keys== 1) { # Great, uniquely found the right key
p "Resolved config key '$key' to '$keys[0]'";
$key= $keys[0];
} elsif( @keys> 1) {
warn "Ambiguous config directive '$key' (@keys)\n";
return LDAP_PARAM_ERROR
} else { #elsif( @keys== 0) {
warn "Unknown config directive '$key'\n";
return LDAP_PARAM_ERROR
}
}
# Push config line, barely processed, to the config stack
# (Note: but not if it's one of the save/load/reset/etc commands).
push @{ $this->{stack}}, [ $key, @val] if
$key ne 'save' and $key ne 'reset' and $key ne 'load' and
$key!~ qr/dump$/o;
# Apply generic changes/replacements we do for every line and
# field. (Basically just a series of convenience helpers).
for( @val) {
# Parse/expand variables if parsing currently enabled.
# ${var} expands to variables, %{directive} to values of
# scalar config directives.
if( $this->{parse}) {
s/\$\{(\S+?)\}/$$this{var}{$1}[0]/g;
s/\%\{(\S+?)\}/$$this{$1}/g;
}
}
# Very simple: if we know about this key, allow it. If not, throw a fit.
unless( defined $this->{$key}) {
return LDAP_PARAM_ERROR
}
# Now handle config directives that call for immediate work as soon
# as they're encountered:
if( $key eq 'message') { # MESSAGE
warn 'Message: ', join( ' ', @val), "\n"
} elsif( $key eq 'schemaldif') { # SCHEMA LDIF
my $schema= $this->{schema};
for( @val) {
p "Parsing schema file '$_'";
unless( $schema->parse( $_)) {
my $error= $schema->error;
warn "Error parsing schema '$_' ($error)\n";
return LDAP_OPERATIONS_ERROR if $this->{schemafatal}
}
}
} elsif( $key eq 'var') { # SET VARIABLE
for( my $i= 0; $i< @val; $i+= 2) {
$this->{var}{$val[$i]}= [ $val[$i+1]];
}
} elsif( $key eq 'cacheread') { # DISKREAD CACHE
$this->parse_cache_opt( 'read', $val[0])
} elsif( $key eq 'reset' and CFG_STACK) { # RESET STACK
$this->{stack}= undef
} elsif( $key eq 'save' and CFG_STACK) { # SAVE STACK
for( @val) {
s/[^\w\.]//g; # allow only [\w\.]+ in filename
s/^\.//; # delete all '.' prefix on the filename
# XXX idea: generic write routine that knows how to write
# plain file, entry, and ldif
my $ret= $this->write_file(
$this->{tmpdir}, $_, Dumper $this->{stack});
return $ret unless $ret== LDAP_SUCCESS;
}
} elsif( $key eq 'load' and CFG_STACK) { # LOAD STACK
# In load specification, the first argument is the filename. The rest,
# if present, is a list of PATTERN REPLACEMENT to perform
# on each stored line before sending it to the config processor.
$_= shift @val;
my $orig_fn= $_;
s/[^\w\.]//g; # allow only [\w\.]+ in filename
s/^\.//; # delete all '.' prefix on the filename
if( $orig_fn ne $_) {
p "Stack load filename sanitized to '$_'";
}
# Evaluate Dumper data that we read in
my $edata;
{
use vars qw/$VAR1/;
my( $ret, @data)= $this->read_file( $this->{tmpdir}, $_);
return $ret unless $ret== LDAP_SUCCESS;
my $data= join q{}, @data;
$edata= eval $data;
if( $@) {
warn "Error loading stack file '$_' ($@)\n";
return LDAP_OPERATIONS_ERROR
}
}
if( defined $edata) {
if( ref $edata ne 'ARRAY') {
warn "Loaded stack file '$_', but it's not an arrayref!\n";
return LDAP_OPERATIONS_ERROR
}
# List of substitutions to perform on each line and each
# argument before sending everything to the config processor.
my %substs= @val;
# Now send line by line to the config routine
for my $line( @$edata) {
# Perform any substs specified as 'load FILE PAT REPL...':
for my $arg( @$line) {
while( my( $p, $r)= each %substs) {
$arg=~ s/$p/$r/g;
}
}
$this->config( @$line)
}
} else {
warn "Empty stack file '$_'. Configuration mistake?\n"
}
} elsif( $key eq 'clean' and CFG_STACK) { # DELETE OLD STACK FILES
unless( $this->{tmpdir}) {
warn "Called 'clean' before 'directory' has been set\n";
return LDAP_OPERATIONS_ERROR
}
my $glob= join '/', $this->{tmpdir}, '*';
for my $file( glob $glob) {
p "Unlinking tmp stack file '$file'";
unless( unlink $file) {
warn "Can't unlink tmp stack file '$file' ($!)\n";
return LDAP_OPERATIONS_ERROR
}
}
} elsif( $key eq 'savedump' and CFG_DUMP) { # SAVE DUMP
for( $val[0]) {
if( not my $ret= nstore $this, $this->{tmpdir}. '/'. $_) {
return $ret unless $ret== LDAP_SUCCESS;
}
}
} elsif( $key eq 'loaddump' and CFG_DUMP) { # LOAD DUMP
my $ret;
for( $val[0]) {
if( not $ret= retrieve $this->{tmpdir}. '/'. $_) {
return $ret unless $ret== LDAP_SUCCESS;
}
# Load all keys into $this and effectively restore state
%$this= %{ $ret};
}
}
# Now generic, regular handling of the config keys:
# NOTE: this will happen for "dynamic" options as well (i.e.
# reset/load/save/message etc.), which don't have any benefit from
# that processing. But we don't particularly care about that; we still
# do it, and we just have a side-effect that we remember the name of
# last saved/loaded stack, message printed etc. This might even show
# handy once in the future.
# If key is defined as arrayref, push [a, b, ...] onto it;
# If key is defined as hashref, do name= [a, b, ...];
# Otherwise, perform regular scalar assignment.
if( ref $this->{$key} eq 'ARRAY') {
push @{ $this->{$key}}, [ @val]
} elsif( ref $this->{$key} eq 'HASH') {
my $locname= shift @val;
$this->{$key}{$locname}= [ @val];
} else {
$this->{$key}= join ' ', @val
}
# Now post-handling of options:
# When directory is defined, create tmp/ inside of it to use
# as a temporary directory store for save/load commands.
if( $key eq 'directory') {
$this->{tmpdir}= join '/', $this->{directory}, 'tmp';
if( ! -e $this->{tmpdir} or ! -d $this->{tmpdir}) {
unless( mkpath $this->{tmpdir}) {
warn "Can't mkdir '$this->{tmpdir}' ($!)\n";
return LDAP_OPERATIONS_ERROR
}
}
}
LDAP_SUCCESS
}
# Adding entries
sub add {
my( $this, $ldif)= @_;
my( $ret, $entry)= $this->ldif2e( \$ldif);
return $ret unless $ret== LDAP_SUCCESS;
#
# LDIF now as ENTRY, do any changes
#
# Normalize DN
$this->normalize( $entry);
#
# Save ENTRY
#
my $dn= $entry->dn;
DEBUG and p "ADD '$dn': ". ( Dumper \$ldif);
$ret= $this->save( $dn, $entry);
return $ret unless $ret== LDAP_SUCCESS;
#
# See if there's work for relocator.
#
if( RELOCATOR) {
$ret= $this->check_relocation( $entry);
return $ret unless $ret== LDAP_SUCCESS;
}
#
# See if there's work for server-side prompter.
#
if( PROMPTER) {
$ret= $this->check_prompter( $entry);
return $ret unless $ret== LDAP_SUCCESS;
}
LDAP_SUCCESS
}
# Searching for entries
sub search {
my $this= shift;
$this->setup_state( \@_);
my( %req, @attrs);
( @req{qw/base scope deref size time filter attrOnly/}, @attrs)=@_;
# Explanation of input parameters:
#
# BASE search base
# SCOPE (0-3) base, one, sub, children
# base: just the one
# one: 1-level sub, no base
# sub: base + sub
# children: sub, no base
# DEREF (0-3) never, always, search, find
# search: only on search
# find: only the base object
# TIMELIMIT: secs, 0 - unlimited, max - max
# SIZELIMIT: nr. entries limit. 0 -unlimited, max - max
# FILTER: dfl (objectClass=*)
# ATTRONLY - attributes only, no values
# @ATTRS list of attrs to return, special: */null= all, += operational
# XXX
$req{'size'}= 6600 if not $req{'size'} or $req{'size'} eq 'max';
$req{'time'}= 6600 if not $req{'time'} or $req{'time'} eq 'max';
p '-' x 78;
my $search = qq|ldapsearch -x -b '$req{base}' -s $SCOPE2NAME{$req{scope}} -a $DEREF2NAME{$req{scope}} -z $req{size} -l $req{time} '$req{filter}' @attrs|;
p "SEARCH: $search";
# Normalize base DN
$this->normalize( \$req{base});
#
# Let's see if we have to do any substitution on input params. SearchSubst
# allows one to match arbitrary parameters of the search request, and if
# all of them satisfy, then perform arbitrary substitutions on the params.
#
my( $id, $i, $ok, $k, $v, $r, @stack)= ( 0);
for my $rule( @{ $this->{searchsubst}}) {
$id++;
( $i, $ok, $k, $v, $r)= ( 0, 1, undef, undef, undef); # Clear vars
$#stack= -1; # Clear stack
# Phase 1: see if all subst conditions match
do {
# XXX error ckin, make sure $i/$i+1 are valid
( $k, $v)= ( $$rule[$i], $$rule[$i+1]);
# If rule matches, save eventual matches to @stack
if( $req{$k}=~ /$v/) {
push @stack, [ $1, $2, $3, $4, $5, $6, $7, $8, $9];
} else {
pd "SEARCH SUBST #$id skipped ($k!~ /$v/)";
$ok= 0;
}
} while( $ok and $i+= 2 and $$rule[$i] ne '->');
next if !$ok; # if this rule doesn't match, search further
# Phase 2: now we know all conditions matched, so perform actual substs
pd "SEARCH SUBST #$id matched '@$rule'";
$i++; # Skip the '->' marker
do {
( $k, $v, $r)= ( $$rule[$i], $$rule[$i+1], $$rule[$i+2]);
$v=~ s/(?<!\\)\$\[(\d+)\]\[(\d+)\]/$stack[$1][$2]/g;
$r=~ s/(?<!\\)\$\[(\d+)\]\[(\d+)\]/$stack[$1][$2]/g;
$req{$k}=~ s/$v/$r/; # <- substs performed here (XXX /g needed?)
pd "SEARCH SUBST #$id action $k=~ s/$v/$r/ RESULT $req{$k}";
} while(
$i+= 3
and defined $$rule[$i]
and defined $$rule[$i+1]
and defined $$rule[$i+2]
);
}
# Now, continue search as normal as if nothing ever happened
DEBUG && do {
my $rewrtn = qq|ldapsearch -x -b '$req{base}' -s $SCOPE2NAME{$req{scope}} -a $DEREF2NAME{$req{scope}} -z $req{size} -l $req{time} '$req{filter}' @attrs|;
p "REWRTN: $rewrtn" if $search ne $rewrtn;
};
# Save original requested base. (Need to have it, unmodified, for proper
# expansion of "." (dots) in DN specifications). Note that this is the
# base AFTER rewriting.
$req{origbase}= $req{base};
# We were letting OpenLDAP handle filtering with filterSearchResults
# directive, but that wasn't optimal because we weren't able to modify
# search filter. Now we do filtering ourselves and we can do
# anything we want anywhere we want (with filter and all other search
# params), producing only final results for passing back onto slapd.
my $filter;
unless( $filter= Net::LDAP::Filter->new( $req{filter})) {
warn "Invalid filter '$req{filter}'\n";
return LDAP_FILTER_ERROR
}
my @matches= ();
my( $ret, $newbase, %ret);
my( $ldif, $entry);
# slapd expects results in LDIF, but our internal subinvocations basically
# always want entry results, not LDIF. So, in a direct call from slapd,
# $as_ldif will be true, otherwise false (implying we want entry objects).
my $as_ldif= $this->{level}== 0? 1: 0;
# First entry is always the base, if -s base or -s sub was specified
# for search scope.
if( $req{scope}== BASE or $req{scope}== SUB) {
# origdn is the original DN, which will be used for "." (dots" expansion
# in DN specs (i.e. cn=abc...)
( $ret, $newbase, $ldif, $entry)=
$this->load( $req{base}, qw/entry 1 ldif 1/, 'origdn', $req{origbase});
return $ret unless $ret== LDAP_SUCCESS;
# If original search base was found, this is a no-op. Otherwise
# $newbase is some fallback base found and we "switch" to it.
$req{base}= $newbase;
# We unshift because on return from Perl to ldap, data is read
# in reverse order.
if( $filter->match( $entry)) {
DEBUG and p "SEARCH ($this->{level}) MATCH:", $entry->dn;
unshift @matches, $as_ldif? $ldif: $entry;
goto SIZE_LIMIT if @matches> $req{size};
}
my $time= time;
goto TIME_LIMIT if any{ $time- $_> $req{time}} @{ $this->{start}}
}
# Further entries may follow unless only base was specifically
# requested with -s base
if( $req{scope}!= BASE){
my $level= 0;
( $ret, $req{base})= $this->resolve( $req{base}, \%ret, qw/leaf 0/);
return $ret unless $ret== LDAP_SUCCESS;
my $dir= $ret{directory};
my $md= $S2L{$req{scope}};
# Use File::Find::Rule to traverse the directory tree selectively
# and scoop out what we want.
File::Find::Rule->file()
->name( '*'. $this->{extension})
->extras({ follow => 1})
->exec( sub{
# Note that here we don't pass origdn onto load(). If a specific
# search base is known and requested, we take that as origdn
# (such as shown above). But if we go into tree subsearch,
# then each time we use DN of the entry found (treating it
# as a specific search base).
( $ret, undef, $ldif, $entry)= $this->load(
$_[2], qw/dnasfile 1/);
return $ret unless $ret== LDAP_SUCCESS;
if( $filter->match( $entry)) {
DEBUG and p "SEARCH ($this->{level}) MATCH:", $entry->dn;
unshift @matches, $as_ldif? $ldif: $entry;
goto SIZE_LIMIT if @matches> $req{size};
}
my $time= time;
goto TIME_LIMIT if any{ $time- $_> $req{time}} @{ $this->{start}}
} )
->maxdepth( $md)
->readable
->in( $dir)
}
$ret= LDAP_SUCCESS;
goto SEARCH_DONE;
TIME_LIMIT:
$ret= LDAP_TIMELIMIT_EXCEEDED;
goto SEARCH_DONE;
SIZE_LIMIT:
$ret= LDAP_SIZELIMIT_EXCEEDED;
SEARCH_DONE:
my ( $level, $start)= ( $this->{level}, $this->{start}[ $this->{level}]);
p "SEARCH ($this->{level}) TOTAL:", scalar @matches, 'matches ('.
"time=". ( time- $start). "/$req{time}, ".
"size=". ( scalar @matches). "/$req{size})";
$this->{level}-= 1;
# $ret will be 0 (LDAP_SUCCESS) if no limits were hit.
( $ret, @matches)
}
# Modifying existing entries
sub modify {
my( $this, $dn, @list)= @_;
my $ldif;
$this->setup_state( \@_);
# Normalize DN
$this->normalize( \$dn);
DEBUG and p "MODIFY '$dn': ". ( Dumper \@list);
my( $ret, $newdn, %ret, $fh, $entry, $orig);
# Load existing entry. In the beginning, this was done with load() so
# that all dynamic work is honored. However, that approach has a problem:
# entry is appended with attributes from the default entry (if any), then
# modified and saved back to disk. So if you had an entry with 5
# regular and 10 appended attributes, and you modify one of those 5, you
# end up with that one modified, and the other 10 copied and added without
# your control (instead of having them kept away in the default entry).
# So what we do now is:
# - load with resolve() which does not expand or run overlays
# - modify entries
# - save back to disk.
# If it happens that a person modifies one of the attributes that came
# in via append, we open another copy of the entry (entry2), this time with
# load(), import the missing value from it (for absolute transparency),
# and then let modify continue. In that case, the modified attribute
# would end up in the original entry, copied over and then modified.
( $ret, $newdn)= $this->resolve( $dn, \%ret, 'entry', 1);
return $ret unless $ret== LDAP_SUCCESS;
( $entry, $fh)= @ret{qw/entry fh/};
$orig= $entry->clone; # For comparison when modifysmarts==1
# Indicator whether we opened another copy of the entry with load()
# and placed it in $entry2.
my $loaded= 0;
my $entry2;
# Perform changes on the in-memory entry
while( @list > 0) {
my( $action, $key)= ( shift @list, shift @list);
my @values;
while( @list) {
# Ignore undefined values. If a key had only one value and it was
# undefined, it'll get deleted due to if( scalar @values) check below.
if( defined $list[0]) {
if( $list[0] !~ qr/^(ADD|DELETE|REPLACE)$/o) {
my $attr= shift @list;
push @values, $attr;
# Make sure that the attr we will operate on exists in the
# entry. Actually, if it's not there, we just try to load it
# from $entry2 and continue (it's still possible that it won't
# exist even after that). No error checking is done here, as
# that'll be handled by the actual modification routine below.
if( !$entry->exists( $attr)) {
# If we didn't load() yet, do it now. Note that this could be
# done more cleanly outside of the loop, but we don't want to
# open every entry twice unless there's a real need to operate
# on entry2.
if( !$loaded) {
( $ret, undef, undef, $entry2)= $this->load( $dn, qw/entry 1/);
return $ret unless $ret== LDAP_SUCCESS;
}
# Now, if the attribute does exist in $entry2, it means it's
# one of the appended attributes, so we add it to $entry, and
# the actual modification routine below can operate on it
# flawlessly.
# We also don't need to worry that get_value() will return an
# empty list because we have the exists() check.
if( $entry2->exists( $attr)) {
$entry->add( $attr=> $entry2->get_value( $attr));
}
}
} else {
last
}
} else {
shift @list
}
}
$action= lc $action;
next unless $key;
if( scalar @values) {
if( not $entry->$action( $key, [@values])) {
warn "Unable to perform $action($key, ...) on '$newdn'\n";
return LDAP_OPERATIONS_ERROR
}
}
else {
# If there are no values, delete key
if( not $entry->delete( $key)) {
warn "Unable to perform delete($key) on '$newdn'\n";
return LDAP_OPERATIONS_ERROR
}
}
}
# Changes to the in-memory entry have been performed correctly.
# We can now remove the old entry and create a new one.
# NOTE: we do NO-OP if modifysmarts==1 and entry is the same after
# change. (Debconf submits MODIFY lines for every requested key even
# if no values actually changed in it, so detecting this is great).
my $changed= 1;
# For comparison, set DN of old and new entry to be equal as that is not
# the point of difference we care about.
$entry->dn( $dn);
$changed= 0 if $this->{modifysmarts} and $this->dequal( $entry, $orig);
$entry->dn( $newdn);
# If changed (true if we have (1) any request, or (2) modifySmarts== 1
# and real change has been detected.)
if( $changed) {
# If entry existed already, no probs.
# But if it is changed and does not exist (i.e. it comes from a fallback)
# then we look up config option modifyCopyOnWrite. When 1, modification
# is performed and entry is saved to where it belongs (we create&modify it)
# However, if modifyCopyOnWrite is 0, we return LDAP_NO_SUCH_OBJECT.
if( $newdn ne $dn) {
if( $this->{modifycopyonwrite}) {
p "MODIFY AND COPY '$newdn' to '$dn'"
} else {
p "MODIFY WON'T WORK ON FALLBACK NOR CREATE '$dn' ".
'(modifyCopyOnWrite == OFF)';
return LDAP_NO_SUCH_OBJECT
}
}
$ret= $this->dn2leaf( $dn, \%ret);
return $ret unless $ret== LDAP_SUCCESS;
$this->save( $dn, $entry, qw/overwrite 1 modify 1/);
}
$this->{level}-= 1;