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