diff options
author | Olivier Croquette <ocroquette@free.fr> | 2012-12-18 10:05:06 -0500 |
---|---|---|
committer | Chad Horohoe <chorohoe@wikimedia.org> | 2012-12-19 22:28:34 -0500 |
commit | 4b6de14811bd49f2df8e86b65a4e895fea0dc3ef (patch) | |
tree | 297eabb7d17bd5739ce1dfffe2c6769c4c5dcca5 /contrib | |
parent | 09d2e6007e79b0a18951c2f1144e36c26b1c6047 (diff) |
Lightweight LDAP server for debugging
Change-Id: I4550d6e2193f97d317cd3e04fe5808cb2d98229a
Diffstat (limited to 'contrib')
-rw-r--r-- | contrib/fake_ldap.pl | 333 |
1 files changed, 333 insertions, 0 deletions
diff --git a/contrib/fake_ldap.pl b/contrib/fake_ldap.pl new file mode 100644 index 0000000000..5d423a785b --- /dev/null +++ b/contrib/fake_ldap.pl @@ -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(); +}
\ No newline at end of file |