blob: 5d423a785b0b89336e5b3069848fc2edb7596b02 [file] [log] [blame]
Olivier Croquette4b6de142012-12-18 10:05:06 -05001#!/usr/bin/env perl
2
3# Fake LDAP server for Gerrit
4# Author: Olivier Croquette <ocroquette@free.fr>
5# Last change: 2012-11-12
6#
7# Abstract:
8# ====================================================================
9#
10# Gerrit currently supports several authentication schemes, but
11# unfortunately not the most basic one, e.g. local accounts with
12# local passwords.
13#
14# As a workaround, this script implements a minimal LDAP server
15# that can be used to authenticate against Gerrit. The information
16# required by Gerrit relative to users (user ID, password, display
17# name, email) is stored in a text file similar to /etc/passwd
18#
19#
20# Usage (see below for the setup)
21# ====================================================================
22#
23# To create a new file to store the user information:
24# fake-ldap edituser --datafile /path/datafile --username maxpower \
25# --displayname "Max Power" --email max.power@provider.com
26#
27# To modify an existing user (for instance the email):
28# fake-ldap edituser --datafile /path/datafile --username ocroquette \
29# --email max.power@provider2.com
30#
31# To set a new password for an existing user:
32# fake-ldap edituser --datafile /path/datafile --username ocroquette \
33# --password ""
34#
35# To start the server:
36# fake-ldap start --datafile /path/datafile
37#
38# The server reads the user data file on each new connection. It's not
39# scalable but it should not be a problem for the intended usage
40# (small teams, testing,...)
41#
42#
43# Setup
44# ===================================================================
45#
46# Install the dependencies
47#
48# Install the Perl module dependencies. On Debian and MacPorts,
49# all modules are available as packages, except Net::LDAP::Server.
50#
51# Debian: apt-get install libterm-readkey-perl
52#
53# Since Net::LDAP::Server consists only of one file, you can put it
54# along the script in Net/LDAP/Server.pm
55#
56# Create the data file with the first user (see above)
57#
58# Start as the script a server ("start" command, see above)
59#
60# Configure Gerrit with the following options:
61#
62# gerrit.canonicalWebUrl = ... (workaround for a known Gerrit bug)
63# auth.type = LDAP_BIND
64# ldap.server = ldap://localhost:10389
65# ldap.accountBase = ou=People,dc=nodomain
66# ldap.groupBase = ou=Group,dc=nodomain
67#
68# Start Gerrit
69#
70# Log on in the Web interface
71#
72# If you want the fake LDAP server to start at boot time, add it to
73# /etc/inittab, with a line like:
74#
75# ld1:6:respawn:su someuser /path/fake-ldap start --datafile /path/datafile
76#
77# ===================================================================
78
79use strict;
80
81# Global var containing the options passed on the command line:
82my %cmdLineOptions;
83
84# Global var containing the user data read from the data file:
85my %userData;
86
87my $defaultport = 10389;
88
89package MyServer;
90
91use Data::Dumper;
92use Net::LDAP::Server;
93use Net::LDAP::Constant qw(LDAP_SUCCESS LDAP_INVALID_CREDENTIALS LDAP_OPERATIONS_ERROR);
94use IO::Socket;
95use IO::Select;
96use Term::ReadKey;
97
98use Getopt::Long;
99
100use base 'Net::LDAP::Server';
101
102sub bind {
103 my $self = shift;
104 my ($reqData, $fullRequest) = @_;
105
106 print "bind called\n" if $cmdLineOptions{verbose} >= 1;
107 print Dumper(\@_) if $cmdLineOptions{verbose} >= 2;
108 my $sha1 = undef;
109 my $uid = undef;
110 eval{
111 $uid = $reqData->{name};
112 $sha1 = main::encryptpwd($uid, $reqData->{authentication}->{simple})
113 };
114 if ($@) {
115 warn $@;
116 return({
117 'matchedDN' => '',
118 'errorMessage' => $@,
119 'resultCode' => LDAP_OPERATIONS_ERROR
120 });
121 }
122
123 print $sha1 . "\n" if $cmdLineOptions{verbose} >= 2;
124 print Dumper($userData{$uid}) . "\n" if $cmdLineOptions{verbose} >= 2;
125
126 if ( defined($sha1) && $sha1 && $userData{$uid} && ( $sha1 eq $userData{$uid}->{password} ) ) {
127 print "authentication of $uid succeeded\n" if $cmdLineOptions{verbose} >= 1;
128 return({
129 'matchedDN' => "dn=$uid,ou=People,dc=nodomain",
130 'errorMessage' => '',
131 'resultCode' => LDAP_SUCCESS
132 });
133 }
134 else {
135 print "authentication of $uid failed\n" if $cmdLineOptions{verbose} >= 1;
136 return({
137 'matchedDN' => '',
138 'errorMessage' => '',
139 'resultCode' => LDAP_INVALID_CREDENTIALS
140 });
141 }
142}
143
144sub search {
145 my $self = shift;
146 my ($reqData, $fullRequest) = @_;
147 print "search called\n" if $cmdLineOptions{verbose} >= 1;
148 print Dumper($reqData) if $cmdLineOptions{verbose} >= 2;
149 my @entries;
150 if ( $reqData->{baseObject} eq 'ou=People,dc=nodomain' ) {
151 my $uid = $reqData->{filter}->{equalityMatch}->{assertionValue};
152 push @entries, Net::LDAP::Entry->new ( "dn=$uid,ou=People,dc=nodomain",
153 , 'objectName'=>"dn=uid,ou=People,dc=nodomain", 'uid'=>$uid, 'mail'=>$userData{$uid}->{email}, 'displayName'=>$userData{$uid}->{displayName});
154 }
155 elsif ( $reqData->{baseObject} eq 'ou=Group,dc=nodomain' ) {
156 push @entries, Net::LDAP::Entry->new ( 'dn=Users,ou=Group,dc=nodomain',
157 , 'objectName'=>'dn=Users,ou=Group,dc=nodomain');
158 }
159
160 return {
161 'matchedDN' => '',
162 'errorMessage' => '',
163 'resultCode' => LDAP_SUCCESS
164 }, @entries;
165}
166
167
168package main;
169
170use Digest::SHA1 qw(sha1 sha1_hex sha1_base64);
171
172sub exitWithError {
173 my $msg = shift;
174 print STDERR $msg . "\n";
175 exit(1);
176}
177
178sub encryptpwd {
179 my ($uid, $passwd) = @_;
180 # Use the user id to compute the hash, to avoid rainbox table attacks
181 return sha1_hex($uid.$passwd);
182}
183
184my $result = Getopt::Long::GetOptions (
185 "port=i" => \$cmdLineOptions{port},
186 "datafile=s" => \$cmdLineOptions{datafile},
187 "email=s" => \$cmdLineOptions{email},
188 "displayname=s" => \$cmdLineOptions{displayName},
189 "username=s" => \$cmdLineOptions{userName},
190 "password=s" => \$cmdLineOptions{password},
191 "verbose=i" => \$cmdLineOptions{verbose},
192);
193exitWithError("Failed to parse command line arguments") if ! $result;
194exitWithError("Please provide a valid path for the datafile") if ! $cmdLineOptions{datafile};
195
196my @commands = qw(start edituser);
197if ( @ARGV != 1 || ! grep {$_ eq $ARGV[0]} @commands ) {
198 exitWithError("Please provide a valid command among: " . join(",", @commands));
199}
200
201my $command = $ARGV[0];
202if ( $command eq "start") {
203 startServer();
204}
205elsif ( $command eq "edituser") {
206 editUser();
207}
208
209
210sub startServer() {
211
212 my $port = $cmdLineOptions{port} || $defaultport;
213
214 print "starting on port $port\n" if $cmdLineOptions{verbose} >= 1;
215
216 my $sock = IO::Socket::INET->new(
217 Listen => 5,
218 Proto => 'tcp',
219 Reuse => 1,
220 LocalAddr => "localhost", # Comment this line if Gerrit doesn't run on this host
221 LocalPort => $port
222 );
223
224 my $sel = IO::Select->new($sock);
225 my %Handlers;
226 while (my @ready = $sel->can_read) {
227 foreach my $fh (@ready) {
228 if ($fh == $sock) {
229 # Make sure the data is up to date on new every connection
230 readUserData();
231
232 # let's create a new socket
233 my $psock = $sock->accept;
234 $sel->add($psock);
235 $Handlers{*$psock} = MyServer->new($psock);
236 } else {
237 my $result = $Handlers{*$fh}->handle;
238 if ($result) {
239 # we have finished with the socket
240 $sel->remove($fh);
241 $fh->close;
242 delete $Handlers{*$fh};
243 }
244 }
245 }
246 }
247}
248
249sub readUserData {
250 %userData = ();
251 open (MYFILE, "<$cmdLineOptions{datafile}") || exitWithError("Could not open \"$cmdLineOptions{datafile}\" for reading");
252 while (<MYFILE>) {
253 chomp;
254 my @fields = split(/:/, $_);
255 $userData{$fields[0]} = { password=>$fields[1], displayName=>$fields[2], email=>$fields[3] };
256 }
257 close (MYFILE);
258}
259
260sub writeUserData {
261 open (MYFILE, ">$cmdLineOptions{datafile}") || exitWithError("Could not open \"$cmdLineOptions{datafile}\" for writing");
262 foreach my $userid (sort(keys(%userData))) {
263 my $userInfo = $userData{$userid};
264 print MYFILE join(":",
265 $userid,
266 $userInfo->{password},
267 $userInfo->{displayName},
268 $userInfo->{email}
269 ). "\n";
270 }
271 close (MYFILE);
272}
273
274sub readPassword {
275 Term::ReadKey::ReadMode('noecho');
276 my $password = Term::ReadKey::ReadLine(0);
277 Term::ReadKey::ReadMode('normal');
278 print "\n";
279 return $password;
280}
281
282sub readAndConfirmPassword {
283 print "Please enter the password: ";
284 my $pwd = readPassword();
285 print "Please re-enter the password: ";
286 my $pwdCheck = readPassword();
287 exitWithError("The passwords are different") if $pwd ne $pwdCheck;
288 return $pwd;
289}
290
291sub editUser {
292 exitWithError("Please provide a valid user name") if ! $cmdLineOptions{userName};
293 my $userName = $cmdLineOptions{userName};
294
295 readUserData() if -r $cmdLineOptions{datafile};
296
297 my $encryptedPassword = undef;
298 if ( ! defined($userData{$userName}) ) {
299 # New user
300
301 exitWithError("Please provide a valid display name") if ! $cmdLineOptions{displayName};
302 exitWithError("Please provide a valid email") if ! $cmdLineOptions{email};
303
304 $userData{$userName} = { };
305
306 if ( ! defined($cmdLineOptions{password}) ) {
307 # No password provided on the command line. Force reading from terminal.
308 $cmdLineOptions{password} = "";
309 }
310 }
311
312 if ( defined($cmdLineOptions{password}) && ! $cmdLineOptions{password} ) {
313 $cmdLineOptions{password} = readAndConfirmPassword();
314 exitWithError("Please provide a non empty password") if ! $cmdLineOptions{password};
315 }
316
317
318 if ( $cmdLineOptions{password} ) {
319 $encryptedPassword = encryptpwd($userName, $cmdLineOptions{password});
320 }
321
322
323 $userData{$userName}->{password} = $encryptedPassword if $encryptedPassword;
324 $userData{$userName}->{displayName} = $cmdLineOptions{displayName} if $cmdLineOptions{displayName};
325 $userData{$userName}->{email} = $cmdLineOptions{email} if $cmdLineOptions{email};
326 # print Data::Dumper::Dumper(\%userData);
327
328 print "New user data for $cmdLineOptions{userName}:\n";
329 foreach ( sort(keys(%{$userData{$userName}}))) {
330 printf " %-15s : %s\n", $_, $userData{$userName}->{$_}
331 }
332 writeUserData();
333}