aboutsummaryrefslogtreecommitdiff
path: root/contrib/convert-srvtab-db
blob: e05b39401eaeff3b6fcecef1daa5b4a39729f9fd (plain)
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
#!/usr/bin/perl -w
#
# convert-srvtab-db -- Converts a leland_srvtab database to wallet
#
# Written by Russ Allbery <eagle@eyrie.org>
# Copyright 2008
#     The Board of Trustees of the Leland Stanford Junior University
#
# See LICENSE for licensing terms.

##############################################################################
# Modules and site configuration
##############################################################################

require 5.006;
use strict;

use Getopt::Long qw(GetOptions);
use Wallet::ACL;
use Wallet::Server;

# The identity of the user who will be creating the wallet database entries,
# for logging purposes.
our $IDENTITY = 'rra/root@stanford.edu';

# The path to the mappings from CGI principals to home directories, used for
# identifying user- and group-based CGI principals that are no longer being
# used.
our $CGI = '/afs/ir/service/etc/passwd.cgi.lsdb';
our %CGI;

# The path to the allow-extract file for the Kerberos KDCs, listing what
# principals can be cached.  This is used to check for principals marked
# cached in the srvtab database that aren't configured to be cachable on the
# KDC side.
our $CACHED = '/home/eagle/work/puppet/services/s_kdc/files/prod'
    . '/etc/krb5kdc/allow-extract';
our @CACHED;

# The list of principal types that should be treated as host-based.
our %HOST_BASED = map { $_ => 1 }
    qw(HTTP afpserver cifs ftp ident imap ldap lpr nfs pop rcmd sieve smtp
       uniengd webauth);

# Additional allowable principal types.  Anything not on this list, not in
# %HOST_BASED, and not a CGI principal will not be converted.
our %ALLOWED = map { $_ => 1 } qw(service);

# The domain to add to form host-based Kerberos v5 principals from Kerberos v4
# principals.
our $DOMAIN = 'stanford.edu';

# The realm to add when checking against the KDC whitelist.
our $REALM = 'stanford.edu';

# ACL mappings.  By default, ACL groups in the srvtab database are mapped to
# ACL groups in the wallet with the same name, but the following groups are
# exceptions.
our %ACL_MAPPING =
    ('group/cgi'            => 'service/www',
     'group/leland'         => 'ADMIN',
     'group/netstaff-sysad' => 'ADMIN',
     'group/oss'            => 'ADMIN',
     'group/ti-ops'         => 'ADMIN',
     'group/tss-cs'         => 'ADMIN');

# Whether to write the output into the wallet database.  Set to 1 if -w was
# given on the command line.
our $WRITE = 0;

##############################################################################
# Load data files
##############################################################################

# Load the CGI password file into the %CGI hash.  The keys are CGI principals
# and the values are home directory paths.
sub load_cgi {
    open (CGI, '<', $CGI) or die "$0: cannot open $CGI: $!\n";
    local $_;
    while (<CGI>) {
        my ($user, $home) = (split ':')[0,5];
        $CGI{$user} = $home;
    }
    close CGI;
}

# Load the regexes permitting extraction of existing keys, used to check
# cached keytabs.
sub load_cached {
    open (CACHED, '<', $CACHED) or die "$0: cannot open $CACHED: $!";
    local $_;
    while (<CACHED>) {
        next if /^\s*\#/;
        next if /^\s*$/;
        s/^\s+//;
        s/\s+$//;
        s/\s*\#.*//;
        push (@CACHED, qr/$_/);
    }
    close CACHED;
}

##############################################################################
# Principal and ACL conversion
##############################################################################

# Convert a Kerberos v4 principal to the corresponding Kerberos v5 principal
# name.  This is somewhat special-cased for the types of principals that we
# had in the srvtab database at Stanford.
sub convert_principal {
    my ($principal) = @_;
    my ($type, $instance) = split (/\./, $principal, 2);
    $type = 'host' if $type eq 'rcmd';
    if (!$instance) {
        $principal = $type;
    } elsif ($HOST_BASED{$type}) {
        $principal = "$type/$instance.$DOMAIN";
    } else {
        $principal = "$type/$instance";
    }
    return $principal;
}

##############################################################################
# Database dump parsing
##############################################################################

# Given a reference to a hash and the file name of a srvtab database dump,
# load that dump into the hash.  The keys will be Kerberos v4 principal names
# and the values will be hashes of key/value pairs from the database.
sub load_dump {
    my ($db, $dump) = @_;
    open (DUMP, '<', $dump) or die "$0: cannot open $dump: $!\n";
    local $_;
    my $last = '';
    while (<DUMP>) {
        if (/^(\S+)$/) {
            $last = $1;
        } elsif (/^\s*(\S+): (\S+)$/) {
            $db->{$last}{$1} = $2;
        }
    }
    close DUMP;
}

# Given the hash containing the srvtab database, delete all entries that have
# never been downloaded and were created more than a week ago.  These can be
# re-requested if they're really needed.  Also delete all entries that are
# hopelessly misnamed and cannot be moved to Kerberos v5, and all entries for
# host-based principals, since we're letting the wallet autocreation handle
# those.
sub clean_database {
    my ($db) = @_;
    my @delete;
    for my $principal (keys %$db) {
        my $entry = $db->{$principal};

        # We were only caching the mail-related keytabs and LDAP keytabs so
        # that users didn't see Kerberos problems when the kvno changed.
        # wallet now deals correctly with those, so don't treat any of those
        # as cached.
        my ($type, $instance) = split (/\./, $principal, 2);
        if ($type =~ /^(imap|ldap|pop|sieve|smtp)$/) {
            delete $entry->{cached};
        }

        # Now check for principals we don't care about.
        unless (   exists $entry->{'srvkeytab-generated-by'}
                or exists $entry->{'srvtab-generated-by'}
                or exists $entry->{'cached'}
                or ($entry->{'created-on'} > time - (7 * 24 * 3600))) {
            push (@delete, $principal);
            next;
        }
        next if ($instance eq 'cgi' and $type ne 'rcmd');
        if (!$instance) {
            push (@delete, $principal);
        } elsif ($HOST_BASED{$type} and not exists $entry->{'cached'}) {
            push (@delete, $principal);
        } elsif (not $HOST_BASED{$type} and not $ALLOWED{$type}) {
            push (@delete, $principal);
        }
    }
    delete @$db{@delete};
}

##############################################################################
# Consistency checking
##############################################################################

# Scan the provided database for anomolies.  Report all of the srvtab database
# objects with anomolies to standard output.
sub check_database {
    my ($db) = @_;
    load_cgi;
    load_cached;
    for my $principal (sort keys %$db) {
        my ($type, $instance) = split (/\./, $principal, 2);
        if ($instance eq 'cgi' and $type ne 'rcmd') {
            if (not $CGI{$type}) {
                print "$principal does not have CGI service\n";
            }
        }
        my $entry = $db->{$principal};
        my @user_keys  = grep { /^srvtab-user-/ } keys %$entry;
        my @group_keys = grep { /^srvtab-acl-/  } keys %$entry;
        my @users  = map { $entry->{$_} } @user_keys;
        my @groups = map { $entry->{$_} } @group_keys;
        if (@users and @groups) {
            print "$principal has both users and groups\n";
        }
        if (@groups > 1) {
            print "$principal has multiple groups\n";
        }
        if ($instance eq 'cgi' and (@users || "@groups" ne 'group/cgi')) {
            print "$principal is CGI principal with weird ACLs\n";
        }
        if ($entry->{cached}) {
            my $k5 = convert_principal ($principal) . '@' . $REALM;
            my $okay;
            for my $regex (@CACHED) {
                if ($k5 =~ /$regex/) {
                    $okay = 1;
                    last;
                }
            }
            print "$principal is cached but not in the KDC config\n"
                unless $okay;
        }
    }
}

##############################################################################
# Database conversion
##############################################################################

# Iterate through the database and convert every entry that doesn't already
# exist in the wallet.
sub convert_database {
    my ($db) = @_;
    my %acls;
    my $server = Wallet::Server->new ($IDENTITY, 'localhost');
    for my $principal (sort keys %$db) {
        my $entry = $db->{$principal};
        my $k5 = convert_principal ($principal);
        if ($server->check ('keytab', $k5)) {
            print "skipping already created principal $k5\n";
            next;
        }
        my @user_keys  = grep { /^srvtab-user-/ } keys %$entry;
        my @group_keys = grep { /^srvtab-acl-/  } keys %$entry;
        my @users  = sort map { $entry->{$_} } @user_keys;
        my @groups = sort map { $entry->{$_} } @group_keys;
        for my $user (@users) {
            $user =~ s/\.?\@.*//;
            $user =~ s,\.,/,;
        }
        my ($owner, $group);
        if (@groups) {
            $owner = $ACL_MAPPING{$groups[0]} || $groups[0];
        } elsif (@users) {
            if ($acls{"@users"}) {
                $owner = $acls{"@users"};
            } elsif (@users == 1) {
                $group = $users[0];
                $group =~ s,/.*,,;
                $group = "user/$group";
            } else {
                $group = $principal;
                $group =~ s/^[^.]+\.//;
                $group = "group/$group";
            }
        }
        if ($group) {
            my $create = 1;
            my $acl = eval { Wallet::ACL->new ($group, $server->dbh) };
            if (defined $acl) {
                my @entries = $acl->list;
                if (grep { $_->[0] ne 'krb5' } @entries) {
                    die "ACL $group exists with unknown types\n";
                }
                @entries = map { $_->[1] } @entries;
                for (@entries) { s/\@\Q$DOMAIN// }
                unless ("@entries" eq "@users") {
                    die "ACL $group exists with different entries\n";
                }
                $create = 0;
            } elsif ($@ !~ /^ACL \S+ not found/) {
                die "unknown ACL error on $group: $@\n";
            }
            $owner = $group;
            $acls{"@users"} = $group;
            if ($WRITE && $create) {
                $server->acl_create ($group) or die $server->error, "\n";
                for my $user (@users) {
                    $server->acl_add ($group, 'krb5', "$user\@$REALM")
                        or die $server->error, "\n";
                }
            } elsif ($create) {
                print "wallet create acl $group\n";
                for my $user (@users) {
                    print "wallet add acl $group krb5 $user\@$REALM\n";
                }
            }
        }
        if ($WRITE) {
            $server->create ('keytab', $k5) or die $server->error, "\n";
            $server->owner ('keytab', $k5, $owner)
                or die $server->error, "\n";
            if ($entry->{cached}) {
                $server->flag_set ('keytab', $k5, 'unchanging')
                    or die $server->error, "\n";
            }
        } else {
            print "wallet create keytab $k5\n";
            print "wallet owner keytab $k5 $owner\n";
            print "wallet flag set keytab $k5 unchanging\n"
                if $entry->{cached};
        }
    }
}

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

# Read in command-line options.
my ($audit, $help);
Getopt::Long::config ('no_ignore_case', 'bundling');
GetOptions ('a|audit' => \$audit,
            'h|help'  => \$help,
            'w|write' => \$WRITE) or exit 1;
if ($help) {
    print "Feeding myself to perldoc, please wait....\n";
    exec ('perldoc', '-t', $0);
}

# Clean up $0 for error reporting.
$0 =~ s%.*/%%;

# Get the dump file.
die "$0: no srvtab database dump file specified" unless @ARGV;
die "$0: too many arguments" if @ARGV > 1;
my ($dump) = @ARGV;
my %db;
load_dump (\%db, $dump);
clean_database (\%db);
print 'Saw ', scalar (keys %db), " total principals\n";

# Perform the requested operation.
if ($audit) {
    check_database (\%db);
} else {
    convert_database (\%db);
}