Merge "Lightweight LDAP server for debugging"
This commit is contained in:
		
							
								
								
									
										333
									
								
								contrib/fake_ldap.pl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										333
									
								
								contrib/fake_ldap.pl
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,333 @@ | ||||
| #!/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(); | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Shawn Pearce
					Shawn Pearce