############ package Sfa; ############ use strict; use warnings; use Dir::Self; use Getopt::Std; use IO::Dir; use JSON; use MIME::Base64; use constant LISTACTION => "list"; use constant BEGINACTION => "begin"; use constant CHECKACTION => "check"; use constant OTPG => "otp-generated"; use constant OTPR => "otp-requested"; use constant CHALLENGE => "challenge"; use constant EXTERNAL => "external"; # This is set in the $self->{e}->{code} word # Currently used to provide the exit status of the command. use constant SUCCESS => 0; use constant FAIL => 1; use constant NOSFA => 2; our($valid_actions) = { LISTACTION,1, BEGINACTION,1, CHECKACTION,1 }; # Global logging object, see Triggerlog at bottom of file to enable/disable. our($log); # We provide all the glue code between the trigger and the # External providers. So it is us that knows the # output format written to stdout, not the calling script. # # All the real code lives in sub modules named after the # service provider. If we add a new provider, copy one # of the existing files and modify to meet your needs. # # My constructor. We get called with the name of # a provider module. We will 'require' this # at runtime, i.e. now. The current model # only allows a single provider to be active # in any one set of triggers, hence we only # load what is used. sub new { my($class) = shift; my($pmod) = @_; my($self) = {}; my($it); bless($self, $class); # Initial error code and message $self->{e} = { message => "Initial error condition", code => 1 }; # Work in progress fields $self->{method} = ""; $self->{token} = ""; $self->{user} = ""; $self->{host} = ""; $self->{action} = ""; $self->{challenge} = ""; $self->{message} = ""; $self->{provider} = undef; if (defined($pmod)) { eval "require $pmod"; if ($@) { $self->setcode(FAIL, "Loading of $pmod failed: ($@)"); } else { $self->{provider} = $pmod->new(); $self->setcode(SUCCESS); } } else { $self->setcode(SUCCESS); } return $self; } # See if we have an error. Return truthy if we have. sub iserror { my($self) = shift; return $self->{e}->{code} == FAIL; } # Brief description of the options sub usage { my($msg) =<print($msg); exit(3); } # We get passed all the command arguments which we use to # setup our state. # We insist that the comand is called in this format: # auth.pl list user host # auth.pl begin user host method # auth.pl check user host token sub processargs { my($self) = shift; my($file) = shift; local(@ARGV) = @_; my($opts) = {}; $log = Triggerlog->new(); $log->log("Trigger called", @ARGV); if(!getopts("Ttph", $opts) || $opts->{h}) { usage(); } if ($opts->{t} || $opts->{T}) { # print the triggers and exit my($outstr); if ($opts->{T}) { $outstr = "Triggers:\n"; } $outstr .=<{p}) { $self->printproviders(); exit(0); } $self->setcode(FAIL, "Processargs initialization failure"); my($action) = $ARGV[0]; $action = "Undefined" if !defined($action); if (!exists($valid_actions->{$action})) { $self->setcode(FAIL, "$action is not a valid action"); return; } $self->{action} = $action; $self->{user} = $ARGV[1]; $self->{host} = $ARGV[2]; if ($action ne "list") { $self->{method} = from_perforce($ARGV[3]); } $self->setcode(SUCCESS); } sub reportandquit { my($self) = shift; if ($self->iserror()) { print($self->{e}->{message}, "\n"); } else { if ($self->{action} eq LISTACTION) { my($obj); my($outstr); $obj = { status => $self->{e}->{code}, methodlist => [] }; my($it); if (!$self->{e}->{code}) { foreach $it (@{$self->{methods}}) { push(@{$obj->{methodlist}}, [ to_perforce($it->{method}), $it->{description} ]); } $outstr = to_json($obj); $log->log("List being returned to trigger", $outstr); print("$outstr\n"); } } elsif ($self->{action} eq BEGINACTION) { my($method) = $self->{method}->{method}; my($meths) = $self->{provider}->{methods}; my($scheme) = $meths->{$method}->{scheme}; my($token) = to_perforce({method => $method, token => $self->{token}}); my($message) = $self->{message}; my($challenge) = $self->{challenge}; my($obj) = { status => 0, }; if ($scheme) { $obj->{scheme} = $scheme; } if ($challenge) { $obj->{challenge} = $challenge; } if ($message) { $obj->{message} = $message; } $obj->{token} = $token; my($outstr) = to_json($obj); $log->log("Begin data being returned to trigger", $outstr, "Code", $self->{e}->{code}); print("$outstr\n"); } elsif ($self->{action} eq CHECKACTION) { my($obj) = { status => $self->{e}->{code}, }; if ($self->{message}) { $obj->{message} = $self->{message}; } my($outstr) = to_json($obj); $log->log("Check data being returned to trigger", $outstr, "Code", $self->{e}->{code}); print("$outstr\n"); } else { print("Unknown action\n"); exit(1); } } exit(0); } sub process { my($self) = shift(); $self->setcode(FAIL, "Initial error state in process"); if ($self->{action} eq LISTACTION) { # The list method is called before we # have setup our methods. $self->{provider}->list($self); return; } my($pro) = $self->{provider}; my($mobj) = $pro->{methods}->{$self->{method}->{method}}; # call either the 'begin' or 'check' function # for our method. # It is up to the callee to set the error and # messages directly via the $self argument. &{$mobj->{$self->{action}}}($pro, $self); } sub setcode { my($self) = shift; my($code, $mess) = @_; $self->{e}->{code} = $code; if (defined($mess)) { $self->{e}->{message} = $mess; } return; } sub setchallenge { my($self) = shift; my($chall) = @_; $self->{challenge} = $chall; } sub setmessage { my($self) = shift; my($mess) = @_; $self->{message} = $mess; } # Used by a provider module to set the methods it # supports after the 'pre-auth' call. sub setmethods { my($self) = shift; my($mlist) = @_; $self->{methods} = $mlist; } # Used by the provider to stuff state information into the # token string passed back to the trigger called and # sent back to us for the 'check' trigger. sub settoken { my($self) = shift; my($token) = @_; $self->{token} = $token; } # Help the user by listing all the provider # modules and details that we know about, those # that are listed in the Sfa module here. sub printproviders { my($self) = shift; my($dh) = IO::Dir->new(__DIR__ . "/Sfa"); my($list) = []; my($it); if (defined($dh)) { my($mod); while ($mod = $dh->read()) { if ($mod =~ /\.pm$/) { $mod =~ s/\.pm//; $mod = "Sfa::$mod"; eval "require $mod"; if (!$@) { no strict "refs"; my($sym) = "${mod}::Servicedetails"; push(@$list, ${$sym}); } else { print("require failed $@\n"); } } } } foreach $it (@$list) { print($it->{name}, " ", $it->{description}, "\n"); } } # Read the password/challenge/passcode from the stdin of the user. sub getpasscode { my($self) = shift(); my($line) = "STDIN"->getline(); chomp($line); return $line; } # We are going to send this object back to the # Perforce server in json format and we want # to preserver the '"' when it comes back. # So we Base64 encode it. sub to_perforce { my($obj) = @_; my($str) = to_json($obj); $log->log("JSON encode for to_perforce", $str); $str = encode_base64($str, ""); return $str; } sub from_perforce { my($str) = @_; my($json) = decode_base64($str); $log->log("JSON decode for from_perforce", $json); my($obj) = from_json($json); return $obj; } ################### package Triggerlog; ################### # Disable logging by setting this to 0 our($enabled) = 1; our($lname) = "/tmp/authfsa_trig.txt"; sub new { my($class) = shift; my($self) = {}; bless($self, $class); if ($enabled) { my($fh) = IO::File->new(); if ($fh->open($lname, ">>")) { $self->{fh} = $fh; } } return $self; } use POSIX qw(strftime); sub log { my($self) = shift; if (exists($self->{fh})) { my(@args) = @_; my($stamp) = strftime("%Y-%m-%d_%H:%M:%S", gmtime()); @args = map({ chomp($_); $_ =~ s/\n/ /; $_} @args); unshift(@args, $stamp); $self->{fh}->print(join("|", @args), "\n"); } } # Modules must return truthy 1;