ccc4c56ef4e8c886c9a5569cb8b6846baf0b896c
[itkacl] / itkacl-2.1 / sync-itkacl.pl
1 #! /usr/bin/perl
2 use strict;
3 use warnings;
4 no warnings qw(once);
5 use DBI;
6 use AppConfig;
7 use lib qw(. /etc/itkacl);
8 require 'config.pm';
9
10 my $conf = AppConfig->new();
11 $conf->define('force!');
12 $conf->getopt(\@ARGV);
13
14 exit 0 if (!should_run());
15
16 my $dbh = DBI->connect("dbi:Pg:" .
17         "dbname=" . $itkaclsyncconfig::db_name . ";" .
18         "host= " . $itkaclsyncconfig::db_host,
19         $itkaclsyncconfig::db_user,
20         $itkaclsyncconfig::db_pass)
21 or die "Couldn't connect to database: " . DBI::errstr();
22 $dbh->{RaiseError} = 1;
23
24 # Fetch members of all groups.
25 my %members = ();
26 while (my ($name,$passwd,$gid,$members) = getgrent()) {
27         push @{$members{$name}}, ( split /\s+/, $members );
28 }
29
30 my %access = ();
31 while (my ($name,$passwd,$uid,$gid,$quota,$comment,$gcos,$dir,$shell,$expire) = getpwent()) {
32         # No system users, except those that are explicitly included
33         next if ($uid < $itkaclsyncconfig::minimum_uid && (grep { $name eq $_ } (@itkaclsyncconfig::force_include_users)) == 0);
34
35         # No Samba machine accounts
36         next if $name =~ /\$$/;
37
38         # Initially, nobody has access.
39         $access{$name} = 0;
40
41         # The user is implicitly a member in his/her primary group.
42         my ($grnam) = getgrgid($gid);
43         if (defined($grnam)) {
44                 push @{$members{$grnam}}, $name;
45         } else {
46                 warn "User $name has unknown gid $gid";
47         }
48                 
49         # Everybody is a member of the "<everyone>" group.
50         push @{$members{'<everyone>'}}, $name;
51 }
52
53 my %entries = ();
54 dump_recursively($itkaclsyncconfig::dns_zone . ".", undef, \%access, \%entries);
55
56 do { } while (update_zone() == 0);
57
58 # Don't run again before we've got updates
59 utime(time(), time(), $itkaclsyncconfig::last_sync_file);
60
61 sub update_zone {
62         # Dump the zone in its current form.
63         my %current_entries = ();
64         open ZONE, "dig +nocmd +nostats +nocomments -t axfr $itkaclsyncconfig::dns_zone. \@$itkaclsyncconfig::dns_server |"
65                 or die "dig failed: $!";
66         while (<ZONE>) {
67                 chomp;
68                 /( .*? \Q$itkaclsyncconfig::dns_zone\E \. ) \s+ 10 \s+ IN \s+ A \s+ 127\.0\.0\.1 \s* $/x or next;
69                 $current_entries{$1} = 1;
70
71         }
72         close ZONE;
73
74         if ($? != 0) {
75                 die "dig failed with \$\? = $?";
76         }
77         if (scalar keys %current_entries < 1) {
78                 die "No entries in zone, transfer probably failed.";
79         }
80
81         # Call nsupdate to update DNS.
82         # Note: We limit ourselves to 1000 records at a time due to nsupdate limitations.
83         # If we hit the limit, we'll return 0 to signal that we should try again.
84         my $num_lines = 0;
85
86         open NSUPDATE, "| nsupdate -y $itkaclsyncconfig::dns_key"
87                 or die "nsupdate failed: $!";
88         print NSUPDATE "zone $itkaclsyncconfig::dns_zone.\n";
89         print NSUPDATE "server $itkaclsyncconfig::dns_server\n";
90
91         for my $entry (keys %entries) {
92                 next if (exists($current_entries{$entry}));
93                 last if (++$num_lines == 1000);
94                 print NSUPDATE "update add $entry 10 A 127.0.0.1\n";
95         }
96         for my $entry (keys %current_entries) {
97                 next if (exists($entries{$entry}));
98                 last if (++$num_lines == 1000);
99                 print NSUPDATE "update delete $entry\n";
100         }
101         print NSUPDATE "send\n";
102         close NSUPDATE;
103
104         print "Made $num_lines updates.\n";
105         if ($num_lines >= 1000) {
106                 print "Note: Hit limit of 1000 updates, will continue in a separate transaction.\n";
107                 return 0;
108         }
109         return 1;
110 }
111
112 sub dump_recursively {
113         my ($path, $id, $allowed, $entries) = @_;
114
115         if (defined($id)) {
116                 # Find all changes to the access tree at this level in the tree.
117                 my $q = $dbh->prepare('SELECT entity_type,entity,allow FROM aclentries WHERE object=? ORDER BY entity_type ASC, allow DESC');
118                 $q->execute($id);
119
120                 while (my $ref = $q->fetchrow_hashref) {
121                         my $entity_type = $ref->{'entity_type'};
122                         my $entity = $ref->{'entity'};
123                         my $allow = ($ref->{'allow'} eq 'grant');
124
125                         if ($entity_type eq 'user') {
126                                 if (!exists($allowed->{$entity})) {
127                                         warn "$path has an ACL entry for non-existant user $entity";
128                                 } else {
129                                         $allowed->{$entity} = $allow;
130                                 }
131                         } elsif ($entity_type eq 'group') {
132                                 if (!exists($members{$entity})) {
133                                         warn "$path has an ACL entry for non-existant group $entity";
134                                 } else {
135                                         for my $member (@{$members{$entity}}) {
136                                                 $allowed->{$member} = $allow;
137                                         }
138                                 }
139                         }
140                 }
141         }
142
143         # Output everyone who has access to this path.
144         for my $user (keys %$allowed) {
145                 next if (!$allowed->{$user});
146                 $entries->{"$user.$path"} = 1;
147         }
148
149         # Now, find all children.
150         my $q;
151         if (defined($id)) {
152                 $q = $dbh->prepare('SELECT id,name FROM objects WHERE parent=?');
153                 $q->execute($id);
154         } else {
155                 $q = $dbh->prepare('SELECT id,name FROM objects WHERE parent IS NULL');
156                 $q->execute;
157         }
158
159         while (my $ref = $q->fetchrow_hashref) {
160                 my %allowed_copy = %$allowed;
161                 dump_recursively($ref->{'name'} . "." . $path, $ref->{'id'}, \%allowed_copy, $entries);
162         }
163 }
164
165 # Check if we have updates since last run (or if we're forced)
166 sub should_run {
167         return 1 if ($conf->force);
168         my $last_update = (stat($itkaclsyncconfig::updated_file))[10] or die "Can't get mtime for $itkaclsyncconfig::updated_file";
169         my $last_sync = (stat($itkaclsyncconfig::last_sync_file))[10] or die "Can't get mtime for $itkaclsyncconfig::last_sync_file";
170         my $last_group = (stat('/etc/group'))[10] or die "Can't get mtime for /etc/group";
171         my $last_group_db = (stat('/var/lib/misc/group.db'))[10] or die "Can't get mtime for /etc/group";
172         my $last_passwd = (stat('/etc/passwd'))[10] or die "Can't get mtime for /etc/passwd";
173         return 1 if ($last_sync - 10 < $last_update);
174         return 1 if ($last_sync - 10 < $last_group);
175         return 1 if ($last_sync - 10 < $last_group_db);
176         return 1 if ($last_sync - 10 < $last_passwd);
177
178         return 0;
179 }