#! /usr/bin/env perl =comment Copyright (c) 2013, Perforce Software, Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL PERFORCE SOFTWARE, INC. BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. User contributed content on the Perforce Public Depot is not supported by Perforce, although it may be supported by its author. This applies to all contributions even those submitted by Perforce employees. =cut =comment Sample Perforce external authentication trigger. This program is both the auth-set and auth-check executable. It stores passwords in an admin-specified named file as a serialized Perl data structure, e.g. '$pass.pl'. It protects against concurrent access with a lock file, '$pass.pl.lock'. It logs to '$pass.pl.log'. The administrator must first set a user's password before they can login or change it on their own. Usage: 1. First create an empty password file: `touch pass.pl` 2. Give a user their password: echo -e 'newpass\nnewpass' | \ ./auth_trigger.pl auth-set the_new_user /p4/passwords.pl 3. Update the server's trigger table. The %quote% is uncessesary if you don't have spaces in your path. p4 triggers -o | /bin/grep -v ^# Triggers: as auth-set auth "%quote%/p4/auth_trigger.pl%quote% auth-set %user% \ %quote%/p4/passwords.pl%quote%" ac auth-check auth "%quote%/p4/auth_trigger.pl%quote% auth-check %user% \ %quote%/p4/passwords.pl%quote%" Note: only tested on GNU/Linux. Potential future additions: record failed login attempts, throttling. todo: Determine if things will get too slow for a large site with lots of old passwords. We might want to use separate files per-user. $Author: jason_gibson $ =cut use strict; use warnings FATAL => 'all'; use autodie; use Fcntl ':flock'; use File::Spec; use File::Temp; use Carp 'longmess'; use Data::Dumper; my ( $mode, $user, $data ) = @ARGV; my ( $in1 , $in2 ) = map { chomp; $_ } ; # Make sure the end-user sees something when the trigger definition is wrong. print "Trigger config error." and exit 1 if grep { ! defined $_ } $mode, $user, $data; my $rcs_re = qr /^\$\S+: (.*) \$$/; # Identify ourselves via RCS keywords. This file must be +k for it to work. '$File: //guest/jason_gibson/misc/triggers/auth_trigger.pl $' =~ $rcs_re; my $trigger_path = $1; '$Change: 8462 $' =~ $rcs_re; my $trigger_version = $1; # Note that STDERR doesn't always get sent back to the user. sub logr { open my $fh, ">>$data.log"; flock $fh, LOCK_EX; print $fh ( scalar localtime ) . " $trigger_path\@$trigger_version: @_\n"; close $fh; } sub logrf { logr @_ ; exit 1 } sub fail { print $_[ 0 ]; logrf "failure message for '$user': @_" } #$SIG{ __DIE__ } = sub { my $e = "@_\n" . longmess; print $e; logrf $e }; # A generic message to send back to the user. my $emsg = "auth-set trigger failed. See server-side trigger logfile."; logrf "Invalide mode '$mode'" if $mode !~ /^auth-(check|set)$/; logrf "Data file must be specified with an absolute path." unless File::Spec->file_name_is_absolute( $data ); # To make sure the trigger isn't accidentally pointed at an invalid location. logr "Password file '$data' missing! Must already exist." and fail $emsg unless -e $data; # Use the data file's dir so we can stick our temp files in the same place. my ( $svol, $sdir, $sfile ) = File::Spec->splitpath( $data ); chdir File::Spec->catpath( $svol, $sdir ); open my $LOCK, ">$data.lock"; flock $LOCK, $mode eq 'auth-set' ? LOCK_EX : LOCK_SH; # The data structure is the list of user names, the current password, when # it was set and the list of previously-used passwords. # # $d{ users }{ password => { string }, date => epoch, prior => { passwords } } open my $DATA, "+<$data"; my %d = -s $data ? %{ eval do { local $/; <$DATA> } } : (); logrf "eval failed: $@" if $@; close $DATA; # Note that for 'p4 passwd', the auth-check trigger is called first. This is # why the admin has to manually create the password the first time. if( $mode eq 'auth-check' ) { my $pass = $in1; fail "Password is invalid or is unset." unless exists $d{ $user } and $d{ $user }{ password } eq $pass; # The password matches, but is it too old? (a month) fail "Password expired." if time - $d{ $user }{ date } > 60 * 60 * 24 * 30; logr "$user logged in."; exit; # Success. } # auth-set from now on. my ( $old, $new ) = ( $in1, $in2 ); goto SAVE unless exists $d{ $user }; fail 'Password not updated. Old and new must differ.' if $old eq $new; fail 'May not reuse password.' if exists $d{ $user }{ prior }{ $new }; my $strength = 0; $strength++ if $new =~ /[a-z]/; $strength++ if $new =~ /[A-Z]/; $strength++ if $new =~ /[\W+]/; $strength++ if length $new > 8; fail 'Password strength is comparable to a wet noodle.' if $strength < 4; SAVE: # the updates. $d{ $user }{ prior }{ $new } = 1; $d{ $user }{ date } = time; $d{ $user }{ password } = $new; my $tmp = File::Temp->new( TEMPLATE => "$data.tmp-XXXXXX", UNLINK => 1, SUFFIX => '.pl' ); # Make the above eval easier by avoiding the default "$VAR =" $Data::Dumper::Terse = 1; print $tmp Dumper \ %d; close $tmp; rename $tmp, $data; # Subject to rename()'s limitations, notably on Windows.