Olivier Croquette | 4b6de14 | 2012-12-18 10:05:06 -0500 | [diff] [blame] | 1 | #!/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 | |
| 79 | use strict; |
| 80 | |
| 81 | # Global var containing the options passed on the command line: |
| 82 | my %cmdLineOptions; |
| 83 | |
| 84 | # Global var containing the user data read from the data file: |
| 85 | my %userData; |
| 86 | |
| 87 | my $defaultport = 10389; |
| 88 | |
| 89 | package MyServer; |
| 90 | |
| 91 | use Data::Dumper; |
| 92 | use Net::LDAP::Server; |
| 93 | use Net::LDAP::Constant qw(LDAP_SUCCESS LDAP_INVALID_CREDENTIALS LDAP_OPERATIONS_ERROR); |
| 94 | use IO::Socket; |
| 95 | use IO::Select; |
| 96 | use Term::ReadKey; |
| 97 | |
| 98 | use Getopt::Long; |
| 99 | |
| 100 | use base 'Net::LDAP::Server'; |
| 101 | |
| 102 | sub 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 | |
| 144 | sub 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 | |
| 168 | package main; |
| 169 | |
| 170 | use Digest::SHA1 qw(sha1 sha1_hex sha1_base64); |
| 171 | |
| 172 | sub exitWithError { |
| 173 | my $msg = shift; |
| 174 | print STDERR $msg . "\n"; |
| 175 | exit(1); |
| 176 | } |
| 177 | |
| 178 | sub 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 | |
| 184 | my $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 | ); |
| 193 | exitWithError("Failed to parse command line arguments") if ! $result; |
| 194 | exitWithError("Please provide a valid path for the datafile") if ! $cmdLineOptions{datafile}; |
| 195 | |
| 196 | my @commands = qw(start edituser); |
| 197 | if ( @ARGV != 1 || ! grep {$_ eq $ARGV[0]} @commands ) { |
| 198 | exitWithError("Please provide a valid command among: " . join(",", @commands)); |
| 199 | } |
| 200 | |
| 201 | my $command = $ARGV[0]; |
| 202 | if ( $command eq "start") { |
| 203 | startServer(); |
| 204 | } |
| 205 | elsif ( $command eq "edituser") { |
| 206 | editUser(); |
| 207 | } |
| 208 | |
| 209 | |
| 210 | sub 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 | |
| 249 | sub 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 | |
| 260 | sub 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 | |
| 274 | sub 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 | |
| 282 | sub 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 | |
| 291 | sub 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 | } |