Build system overhaul.
[misc] / shadowfix.in
diff --git a/shadowfix.in b/shadowfix.in
new file mode 100644 (file)
index 0000000..2f5b773
--- /dev/null
@@ -0,0 +1,636 @@
+#! @PERL@
+#
+# Sanitise Linux password and group databases
+#
+# (c) 1998 Mark Wooding
+#
+
+use MdwOpt;
+use FileHandle;
+use POSIX;
+
+#----- Documentation --------------------------------------------------------
+
+=head1 NAME
+
+shadowfix - fix password and group files
+
+=head1 SYNOPSIS
+
+B<shadowfix> I<options>...
+
+=head1 DESCRIPTION
+
+Shadowfix trundles through your various password files and makes sure
+that they're consistent with themselves.
+
+Currently, the checks Shadowfix makes, and their resolutions, are:
+
+=over 4
+
+=item *
+
+Every user named in the password file should have a shadow password entry;
+create a shadow password entry if necessary.
+
+=item *
+
+Every password field in the password file indicates only presence or absence
+of a password; move a realistic-looking password to the shadow password file,
+and ensure that the password entry in the password file is either empty
+(signifying no password) or an `x' character (signifying a password).
+
+=item *
+
+The primary group of each user exists; warn about nonexistent primary groups.
+
+=item *
+
+Every user is a member of his primary group; add the user to the membership
+list of the group where necessary.
+
+=item *
+
+There are no entries in the shadow password file which don't match entries in
+the main password file; delete orphaned shadow password entries.
+
+=item *
+
+Check group and shadow group files for consistency, as for password and
+shadow password files: every group entry has a shadow entry, no passwords in
+the group file, no orphaned shadow group entries.
+
+=item *
+
+The lists of group members are consistent between group and shadow group
+files; edit the shadow group list to match the main group list where
+necessary.
+
+=item *
+
+Group administrators, listed in the shadow group file, are real users; warn
+about nonexistent group administrators.
+
+=back
+
+A lot of the checks above only make sense when shadow password and group
+files are used.  When instructed not to create shadow files, Shadowfix will
+perform the password/group consistency checks as described above.  Also, if
+given shadow files as input, and told not to create shadow files on output,
+Shadowfix will merge the password information back into the main files.
+Obviously, translating shadowed to non-shadowed files involved information
+loss: in particular, information about password expiry and group
+administration is lost.
+
+It's time to examine the command line options.
+
+=over 4
+
+=item B<--passwd=>I<file>
+
+Use I<file> as the main password file.
+
+=item B<--group=>I<file>
+
+Use I<file> as the main group file.
+
+=item B<--shadow=>I<file>
+
+Use I<file> as the shadow password file.
+
+=item B<--gshadow=>I<file>
+
+Use I<file> as the shadow group file.
+
+=item B<--in-passwd=>I<file>
+
+Read main password file entries from I<file>.
+
+=item B<--in-group=>I<file>
+
+Read main group file entries from I<file>.
+
+=item B<--in-shadow=>I<file>
+
+Read shadow password file entries from I<file>.
+
+=item B<--in-gshadow=>I<file>
+
+Read shadow group file entries from I<file>.
+
+=item B<--quiet>
+
+Suppresses Shadowfix's informative messages about what it's doing to your
+passwored files.  Enabling this option is not recommended.
+
+=back
+
+If the input files aren't specified explitly, Shadowfix defaults to trying to
+read the output files.  These default sensibly to the system password and
+shadow files in F</etc>.
+
+Shadowfix knows about locking password files, so C<passwd> and C<vipw> will
+interact with it properly.
+
+=head1 FILES
+
+=over 4
+
+=item F</etc/passwd>, F</etc/shadow>, F</etc/group>, F</etc/gshadow>
+
+System default password and group files.
+
+=back
+
+=head1 BUGS
+
+Shadowfix doesn't understand how to cope with YP password files.  Yellow
+Pages is a security hole; don't use it.
+
+=head1 AUTHOR
+
+Mark Wooding, <mdw@nsict.org>
+
+=cut
+
+#----- Configuration section ------------------------------------------------
+
+$passwd = "/etc/passwd";
+$group = "/etc/group";
+$shadow = "/etc/shadow";
+$gshadow = "/etc/gshadow";
+$passwd_in = $shadow_in = undef;
+$group_in = $gshadow_in = undef;
+
+$suyb = 0;
+
+#----- Subroutines ----------------------------------------------------------
+
+sub hashify {
+  map { $_, 1 } @_;
+}
+
+sub moan {
+  print STDERR "shadowfix: @_\n";
+}
+
+sub uidsort {
+  if ($a eq $b) {
+    return 0;
+  } elsif ($a eq "+") {
+    return +1;
+  } elsif ($b eq "+") {
+    return -1;
+  } elsif (!exists($ubynam{$a})) {
+    return +1;
+  } elsif (!exists($ubynam{$b})) {
+    return -1;
+  } else {
+    return $ubynam{$a}{uid} <=> $ubynam{$b}{uid} || $a cmp $b;
+  }
+}
+
+sub gidsort {
+  if ($a eq $b) {
+    return 0;
+  } elsif ($a eq "+") {
+    return +1;
+  } elsif ($b eq "+") {
+    return -1;
+  } else {
+    return $gbynam{$a}{gid} <=> $gbynam{$b}{gid} || $a cmp $b;
+  }
+}
+
+sub lockfile {
+  my $file = shift;
+  my $mode = shift or 0644;
+  my $fh = new FileHandle;
+
+  $fh->open("${file}.lock", O_WRONLY | O_EXCL | O_CREAT, 0600) or
+    die "couldn't obtain lock file ${file}.lock";
+  $fh->print($$);
+  $fh->close;
+  $fh->open("${file}.edit", O_WRONLY | O_TRUNC | O_CREAT, $mode) or do {
+    unlink "${file}.lock";
+    die "open(${file}.edit): $!";
+  };
+  return $fh;
+}
+
+sub unlockfile {
+  my $file = shift;
+  my $fh = shift;
+  $fh->close;
+
+  # --- See whether the file changed ---
+
+  CHECK: {
+    my ($ofh, $nfh);
+    my ($obuf, $nbuf);
+    my ($osz, $nsz);
+
+    # --- Open the old and new versions for reading ---
+
+    $ofh = new FileHandle $file, O_RDONLY;
+    $nfh = new FileHandle "${file}.edit", O_RDONLY;
+    last CHECK if !$ofh || !$nfh;
+
+    # --- Read blocks from each and compare ---
+
+    BLOCK: for (;;) {
+      $osz = sysread($ofh, $obuf, 4096);
+      $nsz = sysread($nfh, $nbuf, 4096);
+      last CHECK if !defined($osz) || !defined($nsz);
+      last CHECK if $obuf ne $nbuf;
+      last BLOCK if $sz == 0;
+    }
+
+    # --- The files are identical ---
+
+    # moan "file $file is unchanged";
+    unlink("${file}.edit");
+    unlink("${file}.lock");
+    return;
+  }
+
+  # --- Find the current owner ---
+
+  # system("diff -u $file $file.edit");
+
+  if (-e $file) {
+    my ($mode, $uid, $gid);
+    (undef, undef, $mode, undef, $uid, $gid) = stat $file;
+    chmod $mode, "${file}.edit";
+    chown $uid, $gid, "${file}.edit";
+  }
+
+  # --- Move the old file out of the way ---
+
+  !-e $file or rename("${file}", "${file}-") or do {
+    unlink "${file}.lock";
+    unlink "${file}.edit";
+    die "couldn't save backup copy of $file: $!";
+  };
+
+  # --- Move the new one into place ---
+
+  rename ("${file}.edit", "${file}") or do {
+    rename("${file}-", "${file}");     # This shouldn't happen!
+    unlink "${file}.lock";
+    unlink "${file}.edit";
+    die "HELP!!!  couldn't save backup copy of $file: $!";
+  };
+
+  # --- Release the lock ---
+
+  moan "updated $file"
+    unless $suyb;
+  unlink("${file}.lock");
+}
+
+#----- Main code ------------------------------------------------------------
+
+# --- Options parsing ---
+
+$longopts = { 'passwd'         => { return => 'p',  arg => 'opt' },
+             'in-passwd'       => { return => 'ip', arg => 'opt' },
+             'shadow'          => { return => 'ps', arg => 'opt' },
+             'in-shadow'       => { return => 'ips', arg => 'opt' },
+             'group'           => { return => 'g',  arg => 'opt' },
+             'in-group'        => { return => 'ig', arg => 'opt' },
+             'gshadow'         => { return => 'gs', arg => 'opt' },
+             'in-gshadow'      => { return => 'igs', arg => 'opt' },
+             'quiet'           => { return => 'q', negate => 1 } };
+
+$opts = MdwOpt->new("", $longopts, \@ARGV, ['negate', 'noshort']);
+
+OPT: while (($opt, $arg) = $opts->read, $opt) {
+  $passwd = $arg, next OPT if $opt eq 'p';
+  $passwd_in = $arg, next OPT if $opt eq 'ip';
+  $shadow = $arg, next OPT if $opt eq 'ps';
+  $shadow_in = $arg, next OPT if $opt eq 'ips';
+  $group = $arg, next OPT if $opt eq 'g';
+  $group_in = $arg, next OPT if $opt eq 'ig';
+  $gshadow = $arg, next OPT if $opt eq 'gs';
+  $gshadow_in = $arg, next OPT if $opt eq 'igs';
+  $suyb = 1, next OPT if $opt eq 'q';
+  $suyb = 0, next OPT if $opt eq 'q+';
+  die "bad option";
+}
+
+$passwd_in = $passwd unless $passwd_in;
+$shadow_in = $shadow unless $shadow_in;
+$group_in = $group unless $group_in;
+$gshadow_in = $gshadow unless $gshadow_in;
+
+# --- Initialise the user tables ---
+
+%ubynam = %ubyuid = %subynam = ();
+%gbynam = %gbygid = %sgbynam = ();
+
+# --- Slurp the user tables into memory ---
+
+$pw = new FileHandle $passwd_in, O_RDONLY or die "open($passwd_in): $!";
+while ($line = $pw->getline) {
+  chomp $line;
+  my @f = split /:/, $line;
+  $#f = 6;
+  $a = { data => [ @f ], name => $f[0], uid => $f[2], gid => $f[3] };
+  $ubynam{$a->{name}} = $ubyuid{$a->{uid}} = $a;
+}
+$pw->close;
+
+$gr = new FileHandle $group_in, O_RDONLY or die "open($group_in): $!";
+while ($line = $gr->getline) {
+  chomp $line;
+  my @f = split /:/, $line;
+  $#f = 3;
+  $a = { data => [ @f ],
+        members => { hashify(split /,/, $f[3]) },
+        name => $f[0], gid => $f[2] };
+  $gbynam{$a->{name}} = $gbygid{$a->{gid}} = $a;
+}
+$gr->close;
+
+undef $have_shadow;
+if ($shadow_in) {
+  if ($spw = new FileHandle $shadow_in, O_RDONLY) {
+    while ($line = $spw->getline) {
+      chomp $line;
+      my @f = split /:/, $line;
+      $#f = 8;
+      $a = { data => [ @f ], name => $f[0] };
+      $subynam{$a->{name}} = $a;
+    }
+    $spw->close;
+    $have_shadow = 1;
+  } else {
+    die "open($shadow_in): $!" unless $! == ENOENT;
+  }
+}
+
+undef $have_gshadow;
+if ($gshadow_in) {
+  if ($sgr = new FileHandle $gshadow_in, O_RDONLY) {
+    while ($line = $sgr->getline) {
+      chomp $line;
+      my @f = split /:/, $line;
+      $#f = 3;
+      $a = { data => [ @f ],
+            members => { hashify (split /,/, $f[3]) },
+            name => $f[0] };
+      $sgbynam{$a->{name}} = $a;
+    }
+    $sgr->close;
+    $have_gshadow = 1;
+  } else {
+    die "open($gshadow_in): $!" unless $! == ENOENT;
+  }
+}
+
+# --- Check primary group memberships ---
+
+for $u (values %ubynam) {
+  $unam = $u->{name};
+  if (exists $gbygid{$u->{gid}}) {
+    $g = $gbygid{$u->{gid}};
+    unless ($unam eq "+" || exists($g->{members}{$unam})) {
+      moan "user $unam is not a member of his/her primary group"
+       unless $suyb;
+      $g->{members}{$unam} = 1;
+    }
+  } else {
+    moan "user $unam seems to belong to a nonexistant group"
+      unless $suyb;
+  }
+}
+
+# --- Shadow password checks ---
+
+if ($shadow) {
+
+  # --- Full shadowing checks ---
+
+  for $u (values %ubynam) {
+    $unam = $u->{name};
+
+    # --- Ensure there's a shadow password entry ---
+
+    unless ($unam eq "+" || exists($subynam{$unam})) {
+      moan "user $unam not in shadow password file: adding"
+       unless $suyb;
+      $subynam{$unam} = { name => $unam,
+                         data => [$unam,
+                                  $u->{data}[1],
+                                  10205, 0, 99999, 7, "", "", ""] };
+    }
+
+    # --- Mark unloginable shadow password entries ---
+
+    $su = $subynam{$unam};
+    $p = $su->{data}[1];
+    if ($p ne "*" && length($p) > 0 && length($p) < 5) {
+      moan "blanked user ${unam}'s password"
+       unless $suyb;
+      $su->{data}[1] = "*";
+    }
+
+    # --- Blank out normal password entries ---
+
+    if ($unam eq "+") {
+      # Nothing doing
+    } elsif ($p eq "") {
+      $u->{data}[1] = "";
+    } else {
+      $u->{data}[1] = "x";
+    }
+  }
+
+  # --- Remove shadow entries which don't make sense any more ---
+
+  for $su (values %subynam) {
+    $unam = $su->{name};
+    unless (exists($ubynam{$unam})) {
+      moan "user $unam only in shadow password file: deleting"
+       unless $suyb;
+      delete $subynam{$su->{name}};
+    }
+  }
+
+} elsif ($have_shadow) {
+
+  # --- We have shadowing, but aren't writing out entries ---
+
+  for $u (values %ubynam) {
+    $unam = $u->{name};
+    $u->{data}[1] = $subynam{$unam}{data}[1]
+      if exists($subynam{$unam});
+  }
+}
+
+# --- Shadow group checks ---
+
+for $g (values %gbynam) {
+  $gnam = $g->{name};
+
+  # --- Ensure there's a shadow group entry ---
+
+  unless (!$gshadow || $gnam eq "+" || exists($sgbynam{$gnam})) {
+    moan "group $gnam not in shadow group file: adding"
+      unless $suyb;
+    $sgbynam{$gnam} = { name => $gnam,
+                       data => [$gnam,
+                                $g->{data}[1],
+                                "",
+                                $g->{data}[3]],
+                       members => { %{$g->{members}} } };
+  }
+
+  # --- Play games with passwords ---
+
+  if ($gshadow) {
+
+    # --- Mark unloginable shadow group entries ---
+
+    $sg = $sgbynam{$gnam};
+    $p = $sg->{data}[1];
+    if ($p ne "*" && length($p) > 0 && length($p) < 5) {
+      moan "blanked group ${gnam}'s password"
+       unless $suyb;
+      $sg->{data}[1] = "*";
+    }
+
+    # --- Blank out normal passwords ---
+
+    $g->{data}[1] = "x" unless $gnam eq "+";
+
+    # --- Check that the group's administrators exist ---
+
+    if ($sg->{data}[2] ne "" && !$suyb) {
+      my @admins =
+      my $admin;
+      foreach $admin (split(/,/, $sg->{data}[2])) {
+       exists $ubynam{$admin} or
+         moan "user $admin owns group $gnam but doesn't seem to exist";
+      }
+    }
+
+  } elsif ($have_gshadow) {
+    $g->{data}[1] = $sgbynam{$gnam}{data}[1]
+      if exists($sgbynam{$gnam});
+    $sg = undef;
+  }
+
+  # --- The group members should be consistent across both files ---
+
+  for $i (keys %{$g->{members}}) {
+    exists $ubynam{$i} or $suyb or
+      moan "user $i is a member of group $gnam but doesn't seem to exist";
+    unless (!$sg || exists($sg->{members}{$i})) {
+      moan "group $gnam does not include $i in shadow group file: adding"
+       unless $suyb;
+      $sg->{members}{$i} = 1;
+    }
+  }
+  if ($sg) {
+    for $i (keys %{$sg->{members}}) {
+      unless (exists($g->{members}{$i})) {
+       moan "group $gnam does not include $i in main group file: deleting"
+         unless $suyb;
+       delete $sg->{members}{$i};
+      }
+    }
+  }
+}
+
+# --- Remove entries which are only in the shadow file ---
+
+if ($gshadow) {
+  for $sg (values %sgbynam) {
+    $gnam = $sg->{name};
+    unless (exists($gbynam{$gnam})) {
+      moan "group $gnam only in shadow group file: deleting"
+       unless $suyb;
+      delete $sgbynam{$gnam};
+    }
+  }
+}
+
+# --- Fix up the data blocks ---
+
+for $g (values %gbynam) {
+  $g->{data}[3] = join(",", sort uidsort keys %{$g->{members}});
+}
+
+if ($gshadow) {
+  for $sg (values %sgbynam) {
+    $sg->{data}[3] = join(",", sort uidsort keys %{$sg->{members}});
+  }
+}
+
+# --- Output the finished work of art ---
+
+$pw = lockfile($passwd, 0644);
+for $unam (sort uidsort keys %ubynam) {
+  $pw->print(join(":", @{$ubynam{$unam}{data}}), "\n");
+}
+unlockfile($passwd, $pw);
+
+if ($shadow) {
+  $spw = lockfile($shadow, 0640);
+  for $unam (sort uidsort keys %subynam) {
+    $spw->print(join(":", @{$subynam{$unam}{data}}), "\n");
+  }
+  unlockfile($shadow, $spw);
+}
+
+$gr = lockfile($group, 0644);
+for $gnam (sort gidsort keys %gbynam) {
+  $gr->print(join(":", @{$gbynam{$gnam}{data}}), "\n");
+}
+unlockfile($group, $gr);
+
+if ($gshadow) {
+  $sgr = lockfile($gshadow, 0640);
+  for $gnam (sort gidsort keys %sgbynam) {
+    $sgr->print(join(":", @{$sgbynam{$gnam}{data}}), "\n");
+  }
+  unlockfile($gshadow, $sgr);
+}
+
+#----- More subroutines -----------------------------------------------------
+
+sub udump {
+  my $u = shift;
+  printf "name = %s\n", $u->{name};
+  printf "uid = %d, gid = %d\n", $u->{uid}, $u->{gid};
+  printf "data = %s\n", join(":", @{$u->{data}});
+  print "\n";
+}
+
+sub sudump {
+  my $u = shift;
+  printf "name = %s\n", $u->{name};
+  printf "data = %s\n", join(":", @{$u->{data}});
+  print "\n";
+}
+
+sub gdump {
+  my $g = shift;
+  printf "name = %s\n", $g->{name};
+  printf "gid = %d\n", $g->{gid};
+  printf "members = %s\n", join(",", sort uidsort keys %{$g->{members}});
+  printf "data = %s\n", join(":", @{$g->{data}});
+  print "\n";
+}
+
+sub sgdump {
+  my $g = shift;
+  printf "name = %s\n", $g->{name};
+  printf "members = %s\n", join(",", sort uidsort keys %{$g->{members}});
+  printf "data = %s\n", join(":", @{$g->{data}});
+  print "\n";
+}
+
+#----- That's all, folks ----------------------------------------------------