File: krb5-sync-backend

package info (click to toggle)
krb5-sync 3.0-4
  • links: PTS, VCS
  • area: main
  • in suites: jessie, jessie-kfreebsd
  • size: 2,488 kB
  • ctags: 731
  • sloc: sh: 11,822; ansic: 6,413; perl: 636; makefile: 129
file content (640 lines) | stat: -rwxr-xr-x 22,237 bytes parent folder | download
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
#!/usr/bin/perl
#
# Manipulate Kerberos password and status change queue.
#
# Provides various utility commands to manipulate the queue of password and
# status changes created by krb5-sync's Kerberos plugin.  Uses the krb5-sync
# utility to process changes.

##############################################################################
# Declarations and site configuration
##############################################################################

use 5.006;
use strict;
use warnings;

use Fcntl qw(LOCK_EX O_WRONLY O_CREAT O_EXCL);
use File::Basename qw(basename);
use Getopt::Long qw(GetOptions);
use IPC::Run qw(run);
use Net::Remctl::Backend;
use Pod::Usage qw(pod2usage);
use POSIX qw(EEXIST);

# Path to the krb5-sync binary.
my $SYNC = '/usr/sbin/krb5-sync';

# Default path to the directory that contains queued changes.
my $QUEUE = '/var/spool/krb5-sync';

# Regular expression prefix to match when ignoring error messages.
my $IGNORE_PREFIX = qr{
    \A krb5-sync: [ ]
    AD [ ] (?:password|status) [ ] change [ ] for [ ] \S+ [ ] failed:
}xms;

# Regexes of error messages to ignore when running in silent mode.  These are
# all error messages that can indicate that the target account doesn't exist
# in Active Directory yet, as opposed to some more serious error.
my @IGNORE = (
    qr{ $IGNORE_PREFIX .* Connection [ ] timed [ ] out \z }xms,
    qr{ $IGNORE_PREFIX .* Authentication error \z }xms,
    qr{ $IGNORE_PREFIX .* for [ ] service_locator \z }xms,
    qr{ $IGNORE_PREFIX .* Operation [ ] not [ ] permitted \z }xms,
    qr{ $IGNORE_PREFIX .* user [ ] .* [ ] not [ ] found [ ] in [ ] \S+\z}xms,
);

##############################################################################
# Writing queue files
##############################################################################

# Lock the queue.  We have to do this around any change to the queue or any
# place where we need a consistent snapshot of the queue.  Note that we use
# flock locking; other callers will have to match.
#
# $queue - Queue directory to use
#
# Returns: The file handle of the queue lock, to pass to unlock_queue
#  Throws: Text exception on failure to open or lock the queue
sub lock_queue {
    my ($queue) = @_;
    open(my $lock_fh, '+>', "$queue/.lock")
      or die "$0: cannot open $queue/.lock: $!\n";
    flock($lock_fh, LOCK_EX)
      or die "$0: cannot lock $queue/.lock: $!\n";
    return $lock_fh;
}

# Unlock the queue.
#
# $lock_fh - The file handle of the queue lock
#
# Returns: undef
#  Throws: Text exception on failure to close the lock file
sub unlock_queue {
    my ($lock_fh) = @_;
    close($lock_fh) or die "$0: cannot unlock queue: $!\n";
    return;
}

# Generate a timestamp for queue file names from the current time.  We want
# something that sorts even if time_t adds another digit (okay, this code
# won't last that long, but anyway...).
#
# Returns: The formatted timestamp for the current time.
sub queue_timestamp {
    my ($sec, $min, $hour, $mday, $mon, $year) = gmtime;
    $mon++;
    $year += 1900;
    return sprintf('%04d%02d%02dT%02d%02d%02dZ',
        $year, $mon, $mday, $hour, $min, $sec);
}

# Write out a new queue file.  We currently hard-code the target system to be
# "ad", since that's the only one that's currently implemented, but we keep
# the data field for future expansion.  The queue file will be written with a
# timestamp for the current time.
#
# $queue     - Queue directory to use
# $principal - Principal to queue an operation for
# $operation - Operation, chosen from enable, disable, or password
# @data      - Additional data to add to the queue file
#
# Returns: undef
#  Throws: Text exception on invalid arguments, write failure, or inability
#          to create a usable queue file name
sub queue {
    my ($queue, $principal, $operation, @data) = @_;

    # Convert the principal to a simple username, used for our queue format.
    my $user = $principal;
    $user =~ s{ @ .* }{}xms;
    $user =~ tr{/}{.};

    # Both enable and disable use the same type in the file name.
    my $type = $operation;
    if ($type eq 'disable') {
        $type = 'enable';
    }

    # Create the filename prefix for the queue file.  "-" and a sequence
    # number from 00 to 99 will be appended.
    my $base = "$queue/$user-ad-$type-" . queue_timestamp();

    # Find the next file name.
    my $lock = lock_queue($queue);
    my ($filename, $file);
    for my $count (0 .. 99) {
        $filename = "$base-" . sprintf('%02d', $count);
        if (sysopen($file, $filename, O_WRONLY | O_CREAT | O_EXCL, 0600)) {
            last;
        }
        if ($! != EEXIST) {
            die "$0: cannot create $filename: $!\n";
        }
    }

    # Write the data to the queue file.
    print {$file} "$user\nad\n$operation\n"
      or die "$0: cannot write to $filename: $!\n";
    for my $data (@data) {
        print {$file} $data or die "$0: cannot write to $filename: $!\n";
        if ($data !~ m{\n}xms) {
            print {$file} "\n" or die "$0: cannot write to $filename: $!\n";
        }
    }
    close($file) or die "$0: cannot flush $filename: $!\n";

    # Done.  Unlock the queue.
    unlock_queue($lock);
    return;
}

# Handle a command that writes out a queue file.  This is the command function
# called by Net::Remctl::Backend for the enable, disable, and password
# commands.  It transforms the arguments into the format expected by the queue
# function.
#
# Due to a bug in Net::Remctl::Backend prior to 3.7, we have to check that we
# got sufficient arguments for the password action.
#
# $operation   - The operation (enable, disable, or password)
# $options_ref - Reference to hash of command-line options
#   directory - The queue directory to use
# $principal   - The principal for which to queue a change
# @data        - Any extra data (for password changes, the password)
#
# Returns: 0, indicating success
#  Throws: Text exception on invalid arguments, write failure, or inability
#          to create a usable queue file name
sub queue_command {
    my ($operation, $options_ref, $principal, @data) = @_;
    my $queue = $options_ref->{directory} || $QUEUE;
    if ($operation eq 'password' && !@data) {
        die "sync password: insufficient arguments\n";
    }
    queue($queue, $principal, $operation, @data);
    return 0;
}

##############################################################################
# Queue listing
##############################################################################

# List all files in the queue and return them as a list of file names.  The
# caller is responsible for locking the queue.
#
# $queue - The queue directory to read
#
# Returns: List of queue file names
#  Throws: Text exception on failure to read the queue
sub queue_files {
    my ($queue) = @_;

    # Read the files, ignoring ones with a leading period.
    opendir(my $dir, $queue) or die "$0: cannot open $queue: $!\n";
    my @files = sort grep { !m{ \A [.] }xms } readdir($dir);
    closedir($dir) or die "$0: cannot close $queue: $!\n";
    return @files;
}

# List the current queue.  Displays the user, the type of event, the
# destination service, and the timestamp.  Sort the events the same way
# they're read when processing the queue.
#
# $options_ref - Reference to hash of command-line options
#   directory - The queue directory to use
#
# Returns: 0, indicating success
#  Throws: Text exception on failure to read the queue
sub list {
    my ($options_ref) = @_;
    my $queue = $options_ref->{directory} || $QUEUE;

    # Read in the files within a queue lock.
    my $lock  = lock_queue($queue);
    my @files = queue_files($queue);
    unlock_queue($lock);

    # Walk through the files and read in the data for each.  We don't hold a
    # lock for this, since it doesn't really matter if things disappear out
    # from under us when listing the queue.
    for my $filename (@files) {
        my ($user, undef, undef, $time) = split(m{-}xms, $filename);
        $time =~ s{^(\d\d\d\d)(\d\d)(\d\d)T(\d\d)(\d\d)(\d\d)Z\z}
                  {$1-$2-$3 $4:$5:$6 UTC}xms;

        # Each data element is on one line.  The first is the user, the second
        # is the domain, the third is the operation, and the fourth is the
        # password for password changes.  Ignore corrupt files.  If we can't
        # open the file, it's probably been processed by another copy running
        # process, so just ignore it.
        if (open(my $file, '<', "$queue/$filename")) {
            my @data = <$file>;
            close($file) or die "$0: cannot read $queue/$filename: $!\n";
            chomp(@data);
            next if @data < 3;
            printf {*STDOUT} "%-8s  %-8s  %-4s  %s\n",
              $user, $data[2], $data[1], $time;
        }
    }
    return 0;
}

##############################################################################
# Queue processing
##############################################################################

# Go through the queue and process each pending event using krb5-sync.
# krb5-sync will remove the files when the processing is successful.  If
# processing any of the queue files of a particular type fails, we skip all
# subsequent queue files of the same type for the same user.
#
# $options_ref - Reference to hash of command-line options
#   directory - The queue directory to use
#   silent    - Filter out error messages indicating a missing user in AD
#
# Returns: 0 if all processing succeeded, 1 otherwise
#  Throws: Text exception on failure to read the queue or spawn the command
sub process {
    my ($options_ref) = @_;
    my $queue = $options_ref->{directory} || $QUEUE;
    my $silent = $options_ref->{silent};

    # Read in the files within a queue lock.
    my $list_lock = lock_queue($queue);
    my @files     = queue_files($queue);
    unlock_queue($list_lock);

    # Walk through the list of files and process each in turn, but keep track
    # of which ones failed with an error and skip processing of other files
    # with the same user, domain, and operation.  We don't hold a lock across
    # the entire operation, since it takes too long, but we do hold a queue
    # lock while dealing with any single file.
    my (%skip, $has_errors);
    for my $filename (queue_files($queue)) {
        my $path = "$queue/$filename";

        # Grab the queue lock.
        my $lock = lock_queue($queue);

        # Skip missing files, since they've probably been processed by some
        # other job running in parallel.
        next if !-f "$path";

        # Skip determinations are based on the first three elements of the
        # file name, which will be the username, domain, and operation with
        # enable and disable smashed to enable.  Be sure the file name is
        # sane.
        my ($id) = ($filename =~ m{ \A ([^-]+-[^-]+-[^-]+)- }xms);
        if (!defined($id)) {
            warn "$0: invalid queue file name $path\n";
            $has_errors = 1;
            next;
        }

        # Skip if we already failed a conflicting change.
        next if $skip{$id};

        # Run the krb5-sync command on the queued change.
        my ($stdout, $stderr);
        run([$SYNC, '-f', $path], q{>}, \$stdout, q{2>}, \$stderr);

        # Done with this file.  Unlock the queue.
        unlock_queue($lock);

        # If the comamnd failed, skip conflicting changes and note errors.
        if ($? != 0) {
            $skip{$id} = 1;
            $has_errors = 1;
        }

        # If in silent mode, filter standard error.  Otherwise, print out
        # everything, including standard output.
        if ($options_ref->{silent}) {
          STDERR:
            for my $line (split(m{\n}xms, $stderr)) {
                for my $ignore (@IGNORE) {
                    next STDERR if $line =~ m{ $ignore }xms;
                }
                print {*STDERR} $line
                  or warn "$0: cannot write to standard error: $!\n";
            }
        } else {
            print {*STDERR} $stderr
              or warn "$0: cannot write to standard error: $!\n";
            print {*STDOUT} $stdout
              or warn "$0: cannot write to standard output: $!\n";
        }
    }

    # Return an exit status.
    return $has_errors ? 1 : 0;
}

##############################################################################
# Queue cleanup
##############################################################################

# Given a number of days, remove all queue files older than that number of
# days.
#
# $options_ref - Reference to hash of command-line options
#   directory - The queue directory to use
# $days        - Maximum age in days, beyond which queue files are removed
#
# Returns: 0 for success, 1 if we failed to unlink any files
#  Throws: Text exception on any failures
sub purge {
    my ($options_ref, $days) = @_;
    my $queue = $options_ref->{directory} || $QUEUE;

    # Lock the queue walk through the queue files and check their age.
    my $has_errors;
    my $lock = lock_queue($queue);
    for my $filename (queue_files($queue)) {
        my $path = "$queue/$filename";
        if (-M $path > $days) {
            if (!unlink($path)) {
                warn "$0: cannot delete $path: $!\n";
                $has_errors = 1;
            }
        }
    }

    # Return an exit status.
    return $has_errors ? 1 : 0;
}

##############################################################################
# Main routine
##############################################################################

# Always flush output.
STDOUT->autoflush;

# Clean up the script name for error reporting.
my $fullpath = $0;
local $0 = basename($0);

# Standard options that all commands take.
my @options = qw(directory|d=s elp|h silent|s);

# The Net::Remctl::Backend configuration for our commands.
my %commands = (
    disable => {
        args_min => 1,
        args_max => 1,
        code     => sub { queue_command('disable', @_) },
        options  => ['directory|d=s'],
        summary  => 'Queue disable of <user> in AD',
        syntax   => '<user>',
    },
    enable => {
        args_min => 1,
        args_max => 1,
        code     => sub { queue_command('enable', @_) },
        options  => ['directory|d=s'],
        summary  => 'Queue enable of <user> in AD',
        syntax   => '<user>',
    },
    list => {
        args_max => 0,
        code     => \&list,
        options  => ['directory|d=s'],
        summary  => 'List pending queued actions',
        syntax   => q{},
    },
    manual => {
        args_max => 0,
        code     => sub { pod2usage(-exitval => 0, -verbose => 2) },
        summary  => 'Show full manual including options',
        syntax   => q{},
    },
    password => {
        args_min => 1,
        args_max => 2,
        code     => sub { queue_command('password', @_) },
        options  => ['directory|d=s'],
        stdin    => 2,
        summary  => 'Queue <user> password change in AD',
        syntax   => '<user> <password>',
    },
    process => {
        args_max => 0,
        code     => \&process,
        options  => ['directory|d=s', 'silent|s'],
        summary  => 'Process pending queued actions',
        syntax   => q{},
    },
    purge => {
        args_min => 1,
        args_max => 1,
        code     => \&purge,
        options  => ['directory|d=s'],
        summary  => 'Delete queued actions older than <days>',
        syntax   => '<days>',
    },
);

# Configure Net::Remctl::Backend.
my $backend = Net::Remctl::Backend->new(
    {
        command     => 'sync',
        commands    => \%commands,
        help_banner => 'Kerberos status synchronization help:',
    }
);

# Dispatch to the appropriate command.
exit($backend->run);
__END__

##############################################################################
# Documentation
##############################################################################

=for stopwords
krb5-sync-backend krb5-sync UTC Allbery timestamp username propagations
Kerberos regexes MERCHANTABILITY NONINFRINGEMENT sublicense

=head1 NAME

krb5-sync-backend - Manipulate Kerberos password and status change queue

=head1 SYNOPSIS

B<krb5-sync-backend> (help|manual)

B<krb5-sync-backend> (disable|enable) [B<-d> I<queue>] I<user>

B<krb5-sync-backend> list [B<-d> I<queue>]

B<krb5-sync-backend> process [B<-s>] [B<-d> I<queue>]

B<krb5-sync-backend> password [B<-d> I<queue>] I<user> ad < I<password>

B<krb5-sync-backend> purge [B<-d> I<queue>] I<days>

=head1 DESCRIPTION

B<krb5-sync-backend> provides an interface to the queue of pending
password and account status changes written by either this utility or by
the synchronization plugin after failures.  It can queue account enables,
disables, or password changes for Active Directory, list the queued
actions, or process the queued actions with B<krb5-sync> (telling it to
take its action from a file).

The queue directory will contain files with names in the format:

    <username>-<domain>-<action>-<timestamp>-<count>

where <username> is the name of the affected account (C</> will be
replaced with C<.> in the file name and the realm will be removed),
<domain> is C<ad>, <action> is either C<enable> (used for both enabling
and disabling accounts) or C<password>, <timestamp> is a ISO 8601
timestamp in UTC, and <count> is a two-digit zero-padded number between 0
and 99 (so that we can handle multiple changes that arrive in the same
second).  Each file contains a queued change in the format described in
krb5-sync(8).

Supported arguments to B<krb5-sync-backend> are:

=over 4

=item disable I<user>

Queue a disable action (in Active Directory, as that's the only system
currently supported for enable and disable) for I<user>.

=item enable I<user>

Queue an enable action (in Active Directory, as that's the only system
currently supported for enable and disable) for I<user>.

=item help

List the supported commands.

=item list

List the current contents of the queue.

=item manual

Display this documentation.

=item process

Process the queue.  All queued actions will be sorted alphanumerically
(which due to the timestamp means that all changes for a particular user of
a particular type will be done in the order queued).  B<krb5-sync> will be
called for each queued action, as long as it continues to succeed.  If it
fails for a queued action, all other actions sharing the same username,
domain, and action will be skipped and queue processing will continue with
the next action that differs in one of those three parameters.

=item password I<user> ad < I<password>

Queue a password change for I<user> in Active Directory, setting their
password to I<password>.  By default, I<password> is read from standard input.
It can also be passed as a command-line argument, but this is less secure
since the password is then readable by anyone on the system who can see the
command-line arguments of processes.

The entire standard input is taken as the password, including any trailing
newlines, so be careful how the password is provided.  If using something like
B<echo>, use C<echo -n> or the C<\c> flag, depending on your system.

=item purge I<days>

Delete all queued actions last modified longer than I<days> days ago.  This
can be used to clean up old failed change propagations in situations where
accounts may be created or have password changes queued that are later
removed and never created in other environments.

=back

=head1 OPTIONS

Options must be specified after the command.

=over 4

=item B<-d> I<queue>, B<--directory>=I<queue>

Use I<queue> as the queue directory instead of the default of
F</var/spool/krb5-sync>.  This also changes the lock file accordingly.  This
option is supported for all commands except C<help> and C<manual>.

=item B<-s>, B<--silent>

This option is only allowed for the C<process> command.  Filter out the
output of B<krb5-sync> to ignore common errors and success messages and
only show uncommon errors.  This option will filter out all output when
B<krb5-sync> is successful and will filter out error messages that
normally indicate the account is missing in Active Directory.  The regexes
can be modified at the start of this script.

=back

=head1 FILES

=over 4

=item F</usr/sbin/krb5-sync>

The path to the B<krb5-sync> utility.  This may be changed at the top of
this script.

=item F</var/spool/krb5-sync>

The default path to the queue.  This must match the queue_dir parameter in
F<krb5.conf> used by the plugin.  It can be changed at the top of this
script.

=item F</var/spool/krb5-sync/.lock>

An empty file used for locking the queue.  When writing to or querying the
queue, B<krb5-sync-backend> will open and lock this file with the Perl flock
function, which normally calls flock(2).  Any other queue writers need to
use the same locking mechanism for safe operation.

=back

=head1 AUTHOR

Russ Allbery <eagle@eyrie.org>

=head1 COPYRIGHT AND LICENSE

Copyright 2007, 2008, 2010, 2012, 2013 The Board of Trustees of the Leland
Stanford Junior University

Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

=head1 SEE ALSO

krb5-sync(8)

The current version of this program is available from its web page at
L<http://www.eyrie.org/~eagle/software/krb5-sync/>.

=cut