#!/usr/bin/perl

# This script automatically runs:
#
#   monkeysphere-authentication update-users <user>
#
# every time it detects a change in an authorized_keys or authorized_user_ids
# file. The update-users command operates on the username that owns the file
# that was updated.
#
# The list of files to monitor is generated from the AUTHORIZED_USER_IDS and
# RAW_AUTHORIZED_KEYS variables found in
# /etc/monkeysphere/monkeysphere-authentication.conf and expanded using a list
# of users on the system.
#
# Additionally, the /var/lib/monkeysphere/user-update/lastchange file is
# monitored. If a change is made to that file, the list of files to monitor is
# re-generated based on a fresh listing of users. If you run a hook on user
# creation and deletion that generates a file in this directory, you can ensure
# that the list of files to monitor is always up-to-date.
#
# On debian system you can install required perl modules with: aptitude install
# libfile-changenotify-perl libfile-spec-perl libconfig-general-perl
#
# This script is designed to run at system start and should be run with root
# privileges.
#
# File::ChangeNotify is cross platform - it will choose a sub class for
# monitoring file system changes appropriate to your operating system (if you
# are running Linux, liblinux-inotify2-perl is recommended).

# FIXME: does this handle revocations and re-keying?  if a sysadmin
# switches over to this arrangement, how will the system check for
# revocations?  Scheduling a simple gpg --refresh should handle
# revocations.  I'm not sure how to best handle re-keyings.

use strict;
use warnings;
use File::ChangeNotify;
use File::Basename;
use File::Spec;
use Config::General;

my $user_update_file = '/var/lib/monkeysphere/user-update/lastchange';
my %watch_files;

my $debug = 0;
if (defined($ENV{MONKEYSPHERE_LOG_LEVEL}) &&
    $ENV{MONKEYSPHERE_LOG_LEVEL} =~ /^debug/i) {
  $debug = 1;
}

sub debug {
  printf STDERR @_
    if ($debug eq 1);
}

sub set_watch_files() {
  my %key_file_locations = get_key_file_locations();
  # get list of users on the system
  while(my ($name, $passwd, $uid, $gid, $gcos, $dir, $shell, $home) = getpwent()) {
    while (my ($key, $file) = each (%key_file_locations)) {
      $file =~ s/%h/$home/;
      $file =~ s/%u/$name/;
      $watch_files{ $file } = $name;
    }
  }
  endpwent();
  $watch_files{ $user_update_file } = '';
}

sub get_key_file_locations {
  # set defaults
  my %key_file_locations;
  $key_file_locations{ 'authorized_user_ids' } = '%h/.monkeysphere/authorized_user_ids';
  $key_file_locations{ 'authorized_keys' } = '%h/.ssh/authorized_keys';

  # check monkeysphere-authentication configuration
  my $config_file = '/etc/monkeysphere/monkeysphere-authentication.conf';
  if (-f $config_file) {
    if (-r $config_file) {
      my %config;
      %config = Config::General::ParseConfig($config_file);
      if (exists $config{'AUTHORIZED_USER_IDS'}) {
        $key_file_locations{'authorized_user_ids'} = $config{'AUTHORIZED_USER_IDS'};
      }
      if (exists $config{'RAW_AUTHORIZED_KEYS'}) {
        $key_file_locations{'authorized_keys'} = $config{'RAW_AUTHORIZED_KEYS'};
      }
    }
  }
  return %key_file_locations;
}

sub get_watcher {
  my @filters;
  my @dirs;

  set_watch_files();
  for my $file (%watch_files) {
    my $dir = dirname($file);
    if ( -d $dir && !grep $_ eq $dir, @dirs ) {
      debug("Watching dir: %s\n", $dir);
      push(@dirs,$dir);
      my $file = basename($file);
      if ( !grep $_ eq $file, @filters ) {
        $file = quotemeta($file);
        debug("Adding file filter: %s\n", $file);
        push(@filters,$file);
      }
    }
  }

  # create combined file filters to limit our monitor
  my $filter = '^(' . join("|",@filters) . ')$';

  # return a watcher object
  return my $watcher =
    File::ChangeNotify->instantiate_watcher
      ( directories => [ @dirs ],
        filter      => qr/$filter/,
      );
}

sub watch {
  my $watcher = get_watcher();
  while ( my @events = $watcher->wait_for_events() ) {
    my %users;
    for my $event (@events) {
      if($event->path eq "$user_update_file") {
        debug("Reloading user list\n");
        $watcher = get_watcher();
      } else {
        # if user deleted, file might not exist
        # FIXME - m-a u returns an error if the username
        # doesn't exist. It should silently ensure that 
        # the generated authorized_keys file is deleted.
        # Once it's fixed, we should execute even if the 
        # file is gone.
        if( -f $event->path) {
          my $username = $watch_files { $event->path };
          $users{ $username } = 1;
        }
      }
    }
    for ((my $username) = each(%users)) {
      debug("Updating user: %s\n", $username);
      # FIXME: this call blocks until m-a u finishes running, i think.
      # what happens if other changes occur in the meantime?  Can we
      # rate-limit this?  Could we instead spawn child processes that
      # run this command directly?
      system('monkeysphere-authentication', 'update-users', $username);
    }
  }
}

watch();
