| #!/usr/bin/env perl |
| |
| # Fake LDAP server for Gerrit |
| # Author: Olivier Croquette <ocroquette@free.fr> |
| # Last change: 2012-11-12 |
| # |
| # Abstract: |
| # ==================================================================== |
| # |
| # Gerrit currently supports several authentication schemes, but |
| # unfortunately not the most basic one, e.g. local accounts with |
| # local passwords. |
| # |
| # As a workaround, this script implements a minimal LDAP server |
| # that can be used to authenticate against Gerrit. The information |
| # required by Gerrit relative to users (user ID, password, display |
| # name, email) is stored in a text file similar to /etc/passwd |
| # |
| # |
| # Usage (see below for the setup) |
| # ==================================================================== |
| # |
| # To create a new file to store the user information: |
| # fake-ldap edituser --datafile /path/datafile --username maxpower \ |
| # --displayname "Max Power" --email max.power@provider.com |
| # |
| # To modify an existing user (for instance the email): |
| # fake-ldap edituser --datafile /path/datafile --username ocroquette \ |
| # --email max.power@provider2.com |
| # |
| # To set a new password for an existing user: |
| # fake-ldap edituser --datafile /path/datafile --username ocroquette \ |
| # --password "" |
| # |
| # To start the server: |
| # fake-ldap start --datafile /path/datafile |
| # |
| # The server reads the user data file on each new connection. It's not |
| # scalable but it should not be a problem for the intended usage |
| # (small teams, testing,...) |
| # |
| # |
| # Setup |
| # =================================================================== |
| # |
| # Install the dependencies |
| # |
| # Install the Perl module dependencies. On Debian and MacPorts, |
| # all modules are available as packages, except Net::LDAP::Server. |
| # |
| # Debian: apt-get install libterm-readkey-perl |
| # |
| # Since Net::LDAP::Server consists only of one file, you can put it |
| # along the script in Net/LDAP/Server.pm |
| # |
| # Create the data file with the first user (see above) |
| # |
| # Start as the script a server ("start" command, see above) |
| # |
| # Configure Gerrit with the following options: |
| # |
| # gerrit.canonicalWebUrl = ... (workaround for a known Gerrit bug) |
| # auth.type = LDAP_BIND |
| # ldap.server = ldap://localhost:10389 |
| # ldap.accountBase = ou=People,dc=nodomain |
| # ldap.groupBase = ou=Group,dc=nodomain |
| # |
| # Start Gerrit |
| # |
| # Log on in the Web interface |
| # |
| # If you want the fake LDAP server to start at boot time, add it to |
| # /etc/inittab, with a line like: |
| # |
| # ld1:6:respawn:su someuser /path/fake-ldap start --datafile /path/datafile |
| # |
| # =================================================================== |
| |
| use strict; |
| |
| # Global var containing the options passed on the command line: |
| my %cmdLineOptions; |
| |
| # Global var containing the user data read from the data file: |
| my %userData; |
| |
| my $defaultport = 10389; |
| |
| package MyServer; |
| |
| use Data::Dumper; |
| use Net::LDAP::Server; |
| use Net::LDAP::Constant qw(LDAP_SUCCESS LDAP_INVALID_CREDENTIALS LDAP_OPERATIONS_ERROR); |
| use IO::Socket; |
| use IO::Select; |
| use Term::ReadKey; |
| |
| use Getopt::Long; |
| |
| use base 'Net::LDAP::Server'; |
| |
| sub bind { |
| my $self = shift; |
| my ($reqData, $fullRequest) = @_; |
| |
| print "bind called\n" if $cmdLineOptions{verbose} >= 1; |
| print Dumper(\@_) if $cmdLineOptions{verbose} >= 2; |
| my $sha1 = undef; |
| my $uid = undef; |
| eval{ |
| $uid = $reqData->{name}; |
| $sha1 = main::encryptpwd($uid, $reqData->{authentication}->{simple}) |
| }; |
| if ($@) { |
| warn $@; |
| return({ |
| 'matchedDN' => '', |
| 'errorMessage' => $@, |
| 'resultCode' => LDAP_OPERATIONS_ERROR |
| }); |
| } |
| |
| print $sha1 . "\n" if $cmdLineOptions{verbose} >= 2; |
| print Dumper($userData{$uid}) . "\n" if $cmdLineOptions{verbose} >= 2; |
| |
| if ( defined($sha1) && $sha1 && $userData{$uid} && ( $sha1 eq $userData{$uid}->{password} ) ) { |
| print "authentication of $uid succeeded\n" if $cmdLineOptions{verbose} >= 1; |
| return({ |
| 'matchedDN' => "dn=$uid,ou=People,dc=nodomain", |
| 'errorMessage' => '', |
| 'resultCode' => LDAP_SUCCESS |
| }); |
| } |
| else { |
| print "authentication of $uid failed\n" if $cmdLineOptions{verbose} >= 1; |
| return({ |
| 'matchedDN' => '', |
| 'errorMessage' => '', |
| 'resultCode' => LDAP_INVALID_CREDENTIALS |
| }); |
| } |
| } |
| |
| sub search { |
| my $self = shift; |
| my ($reqData, $fullRequest) = @_; |
| print "search called\n" if $cmdLineOptions{verbose} >= 1; |
| print Dumper($reqData) if $cmdLineOptions{verbose} >= 2; |
| my @entries; |
| if ( $reqData->{baseObject} eq 'ou=People,dc=nodomain' ) { |
| my $uid = $reqData->{filter}->{equalityMatch}->{assertionValue}; |
| push @entries, Net::LDAP::Entry->new ( "dn=$uid,ou=People,dc=nodomain", |
| , 'objectName'=>"dn=uid,ou=People,dc=nodomain", 'uid'=>$uid, 'mail'=>$userData{$uid}->{email}, 'displayName'=>$userData{$uid}->{displayName}); |
| } |
| elsif ( $reqData->{baseObject} eq 'ou=Group,dc=nodomain' ) { |
| push @entries, Net::LDAP::Entry->new ( 'dn=Users,ou=Group,dc=nodomain', |
| , 'objectName'=>'dn=Users,ou=Group,dc=nodomain'); |
| } |
| |
| return { |
| 'matchedDN' => '', |
| 'errorMessage' => '', |
| 'resultCode' => LDAP_SUCCESS |
| }, @entries; |
| } |
| |
| |
| package main; |
| |
| use Digest::SHA1 qw(sha1 sha1_hex sha1_base64); |
| |
| sub exitWithError { |
| my $msg = shift; |
| print STDERR $msg . "\n"; |
| exit(1); |
| } |
| |
| sub encryptpwd { |
| my ($uid, $passwd) = @_; |
| # Use the user id to compute the hash, to avoid rainbox table attacks |
| return sha1_hex($uid.$passwd); |
| } |
| |
| my $result = Getopt::Long::GetOptions ( |
| "port=i" => \$cmdLineOptions{port}, |
| "datafile=s" => \$cmdLineOptions{datafile}, |
| "email=s" => \$cmdLineOptions{email}, |
| "displayname=s" => \$cmdLineOptions{displayName}, |
| "username=s" => \$cmdLineOptions{userName}, |
| "password=s" => \$cmdLineOptions{password}, |
| "verbose=i" => \$cmdLineOptions{verbose}, |
| ); |
| exitWithError("Failed to parse command line arguments") if ! $result; |
| exitWithError("Please provide a valid path for the datafile") if ! $cmdLineOptions{datafile}; |
| |
| my @commands = qw(start edituser); |
| if ( @ARGV != 1 || ! grep {$_ eq $ARGV[0]} @commands ) { |
| exitWithError("Please provide a valid command among: " . join(",", @commands)); |
| } |
| |
| my $command = $ARGV[0]; |
| if ( $command eq "start") { |
| startServer(); |
| } |
| elsif ( $command eq "edituser") { |
| editUser(); |
| } |
| |
| |
| sub startServer() { |
| |
| my $port = $cmdLineOptions{port} || $defaultport; |
| |
| print "starting on port $port\n" if $cmdLineOptions{verbose} >= 1; |
| |
| my $sock = IO::Socket::INET->new( |
| Listen => 5, |
| Proto => 'tcp', |
| Reuse => 1, |
| LocalAddr => "localhost", # Comment this line if Gerrit doesn't run on this host |
| LocalPort => $port |
| ); |
| |
| my $sel = IO::Select->new($sock); |
| my %Handlers; |
| while (my @ready = $sel->can_read) { |
| foreach my $fh (@ready) { |
| if ($fh == $sock) { |
| # Make sure the data is up to date on new every connection |
| readUserData(); |
| |
| # let's create a new socket |
| my $psock = $sock->accept; |
| $sel->add($psock); |
| $Handlers{*$psock} = MyServer->new($psock); |
| } else { |
| my $result = $Handlers{*$fh}->handle; |
| if ($result) { |
| # we have finished with the socket |
| $sel->remove($fh); |
| $fh->close; |
| delete $Handlers{*$fh}; |
| } |
| } |
| } |
| } |
| } |
| |
| sub readUserData { |
| %userData = (); |
| open (MYFILE, "<$cmdLineOptions{datafile}") || exitWithError("Could not open \"$cmdLineOptions{datafile}\" for reading"); |
| while (<MYFILE>) { |
| chomp; |
| my @fields = split(/:/, $_); |
| $userData{$fields[0]} = { password=>$fields[1], displayName=>$fields[2], email=>$fields[3] }; |
| } |
| close (MYFILE); |
| } |
| |
| sub writeUserData { |
| open (MYFILE, ">$cmdLineOptions{datafile}") || exitWithError("Could not open \"$cmdLineOptions{datafile}\" for writing"); |
| foreach my $userid (sort(keys(%userData))) { |
| my $userInfo = $userData{$userid}; |
| print MYFILE join(":", |
| $userid, |
| $userInfo->{password}, |
| $userInfo->{displayName}, |
| $userInfo->{email} |
| ). "\n"; |
| } |
| close (MYFILE); |
| } |
| |
| sub readPassword { |
| Term::ReadKey::ReadMode('noecho'); |
| my $password = Term::ReadKey::ReadLine(0); |
| Term::ReadKey::ReadMode('normal'); |
| print "\n"; |
| return $password; |
| } |
| |
| sub readAndConfirmPassword { |
| print "Please enter the password: "; |
| my $pwd = readPassword(); |
| print "Please re-enter the password: "; |
| my $pwdCheck = readPassword(); |
| exitWithError("The passwords are different") if $pwd ne $pwdCheck; |
| return $pwd; |
| } |
| |
| sub editUser { |
| exitWithError("Please provide a valid user name") if ! $cmdLineOptions{userName}; |
| my $userName = $cmdLineOptions{userName}; |
| |
| readUserData() if -r $cmdLineOptions{datafile}; |
| |
| my $encryptedPassword = undef; |
| if ( ! defined($userData{$userName}) ) { |
| # New user |
| |
| exitWithError("Please provide a valid display name") if ! $cmdLineOptions{displayName}; |
| exitWithError("Please provide a valid email") if ! $cmdLineOptions{email}; |
| |
| $userData{$userName} = { }; |
| |
| if ( ! defined($cmdLineOptions{password}) ) { |
| # No password provided on the command line. Force reading from terminal. |
| $cmdLineOptions{password} = ""; |
| } |
| } |
| |
| if ( defined($cmdLineOptions{password}) && ! $cmdLineOptions{password} ) { |
| $cmdLineOptions{password} = readAndConfirmPassword(); |
| exitWithError("Please provide a non empty password") if ! $cmdLineOptions{password}; |
| } |
| |
| |
| if ( $cmdLineOptions{password} ) { |
| $encryptedPassword = encryptpwd($userName, $cmdLineOptions{password}); |
| } |
| |
| |
| $userData{$userName}->{password} = $encryptedPassword if $encryptedPassword; |
| $userData{$userName}->{displayName} = $cmdLineOptions{displayName} if $cmdLineOptions{displayName}; |
| $userData{$userName}->{email} = $cmdLineOptions{email} if $cmdLineOptions{email}; |
| # print Data::Dumper::Dumper(\%userData); |
| |
| print "New user data for $cmdLineOptions{userName}:\n"; |
| foreach ( sort(keys(%{$userData{$userName}}))) { |
| printf " %-15s : %s\n", $_, $userData{$userName}->{$_} |
| } |
| writeUserData(); |
| } |