#!/usr/bin/perl ## ## Copyright (c) Enalean, 2015. All Rights Reserved. ## Copyright (c) Xerox Corporation, Codendi Team, 2001-2010. All rights reserved ## ## Tuleap is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published by ## the Free Software Foundation; either version 2 of the License, or ## (at your option) any later version. ## ## Tuleap is distributed in the hope that it will be useful, ## but WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with Tuleap. If not, see . ## ## This script has been written against the Redmine ## see package Apache::Authn::Tuleap; use strict; use warnings FATAL => 'all', NONFATAL => 'redefine'; use Apache2::Module; use Apache2::Access; use Apache2::Connection; use Apache2::ServerRec qw(); use Apache2::RequestRec qw(); use Apache2::RequestUtil qw(); use Apache2::Const qw(:common :override :cmd_how); use APR::Pool (); use APR::Table (); use DBI qw(:sql_types); use Net::LDAP; use Net::LDAP::Util qw(escape_filter_value); use Digest::SHA qw(sha512); my @directives = ( { name => 'TuleapCacheCredsMax', req_override => OR_AUTHCFG, args_how => TAKE1, errmsg => 'TuleapCacheCredsMax must be decimal number', }, { name => 'TuleapDSN', req_override => OR_AUTHCFG, args_how => TAKE1, errmsg => 'Dsn in format used by Perl DBI. eg: "DBI:Pg:dbname=databasename;host=my.db.server"', }, { name => 'TuleapDbUser', req_override => OR_AUTHCFG, args_how => TAKE1, }, { name => 'TuleapDbPass', req_override => OR_AUTHCFG, args_how => TAKE1, }, { name => 'TuleapGroupId', req_override => OR_AUTHCFG, args_how => TAKE1, }, { name => 'TuleapLdapServers', req_override => OR_AUTHCFG, args_how => TAKE1, }, { name => 'TuleapLdapDN', req_override => OR_AUTHCFG, args_how => TAKE1, }, { name => 'TuleapLdapUid', req_override => OR_AUTHCFG, args_how => TAKE1, }, { name => 'TuleapLdapBindDN', req_override => OR_AUTHCFG, args_how => TAKE1, }, { name => 'TuleapLdapBindPassword', req_override => OR_AUTHCFG, args_how => TAKE1, }, ); sub TuleapCacheCredsMax { my ($self, $parms, $arg) = @_; if ($arg) { $self->{TuleapCachePool} = APR::Pool->new; $self->{TuleapCacheCreds} = APR::Table::make($self->{TuleapCachePool}, $arg); $self->{TuleapCacheCredsCount} = 0; $self->{TuleapCacheCredsMax} = $arg; } return; } sub TuleapDSN { my ($self, $parms, $arg) = @_; $self->{TuleapDSN} = $arg; return; } sub TuleapDbUser { my ($self, $parms, $arg) = @_; $self->{TuleapDbUser} = $arg; return; } sub TuleapDbPass { my ($self, $parms, $arg) = @_; $self->{TuleapDbPass} = $arg; return; } sub TuleapGroupId { my ($self, $parms, $arg) = @_; $self->{TuleapGroupId} = $arg; return; } sub TuleapLdapServers { my ($self, $parms, $arg) = @_; $self->{TuleapLdapServers} = $arg; return; } sub TuleapLdapDN { my ($self, $parms, $arg) = @_; $self->{TuleapLdapDN} = $arg; return; } sub TuleapLdapUid { my ($self, $parms, $arg) = @_; $self->{TuleapLdapUid} = $arg; return; } sub TuleapLdapBindDN { my ($self, $parms, $arg) = @_; $self->{TuleapLdapBindDN} = $arg; return; } sub TuleapLdapBindPassword { my ($self, $parms, $arg) = @_; $self->{TuleapLdapBindPassword} = $arg; return; } sub access_handler { my $r = shift; if (!$r->some_auth_required) { $r->log_reason('No authentication has been configured'); return FORBIDDEN; } return OK; } sub authen_handler { my $r = shift; my ($res, $user_secret) = $r->get_basic_auth_pw(); if ($res != OK) { return $res; } if (is_user_allowed($r, $r->user, $user_secret)) { return OK; } else { $r->note_auth_failure(); return AUTH_REQUIRED; } } sub is_user_allowed { my ($r, $username, $user_secret) = @_; my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config); my $project_id = $cfg->{TuleapGroupId}; if (is_user_in_cache($cfg, $username, $user_secret)) { return 1; } my $dbh = DBI->connect($cfg->{TuleapDSN}, $cfg->{TuleapDbUser}, $cfg->{TuleapDbPass}, { AutoCommit => 0 }); my $tuleap_username = $username; if ($cfg->{TuleapLdapServers}) { $tuleap_username = get_tuleap_username_from_ldap_uid($dbh, $username); } if (! $tuleap_username || ! can_user_access_project($dbh, $project_id, $tuleap_username)) { $dbh->disconnect(); return 0; } my $is_user_authenticated = 0; my $token_id = get_user_token($dbh, $tuleap_username, $user_secret); if ($token_id) { $is_user_authenticated = 1; } else { if ($cfg->{TuleapLdapServers}) { $is_user_authenticated = is_valid_user_ldap($cfg, $username, $user_secret); } else { $is_user_authenticated = is_valid_user_database($dbh, $tuleap_username, $user_secret); } } if ($is_user_authenticated) { add_user_to_cache($cfg, $username, $user_secret); if ($token_id) { update_user_token_usage($dbh, $token_id, $r->connection->remote_ip()); } } $dbh->disconnect(); return $is_user_authenticated; } sub is_user_in_cache { my ($cfg, $username, $user_secret) = @_; if (!$cfg->{TuleapCacheCredsMax}) { return 0; } my $user_secret_in_cache = $cfg->{TuleapCacheCreds}->get($username); return (defined $user_secret_in_cache and compare_string_constant_time(hash_user_secret($user_secret), $user_secret_in_cache)); } sub add_user_to_cache { my ($cfg, $username, $user_secret) = @_; if (!$cfg->{TuleapCacheCredsMax}) { return 0; } if ($cfg->{TuleapCacheCredsCount} < $cfg->{TuleapCacheCredsMax}) { my $hashed_user_secret = hash_user_secret($user_secret); $cfg->{TuleapCacheCreds}->set($username, $hashed_user_secret); $cfg->{TuleapCacheCredsCount}++; } else { $cfg->{TuleapCacheCreds}->clear(); $cfg->{TuleapCacheCredsCount} = 0; } return; } sub get_user_token { my ($dbh, $username, $user_secret) = @_; my $query = << 'EOF'; SELECT id, token FROM svn_token JOIN user ON user.user_id=svn_token.user_id WHERE user_name=?; EOF my $statement = $dbh->prepare($query); $statement->bind_param(1, $username, SQL_VARCHAR); $statement->execute(); my $token_id = 0; my $token_secret = 0; while (my ($row_id, $row_secret) = $statement->fetchrow_array()) { if (compare_string_constant_time(crypt($user_secret, $row_secret), $row_secret)) { $token_id = $row_id; $token_secret = $row_secret; last; } } $statement->finish(); undef $statement; return $token_id; } sub update_user_token_usage { my ($dbh, $token_id, $ip_address) = @_; my $query = q/UPDATE svn_token SET last_usage=?, last_ip=? WHERE id=?/; my $statement = $dbh->prepare($query); $statement->bind_param(1, time, SQL_INTEGER); $statement->bind_param(2, $ip_address, SQL_VARCHAR); $statement->bind_param(3, $token_id, SQL_INTEGER); $statement->execute(); $statement->finish(); undef $statement; $dbh->commit(); return; } sub can_user_access_project { my ($dbh, $project_id, $username) = @_; my $query = << 'EOF'; SELECT NULL FROM user WHERE user.status='A' AND user_name=? UNION ALL SELECT NULL FROM user JOIN user_group ON user_group.user_id=user.user_id WHERE user.status='R' AND user_name=? AND user_group.group_id=?; EOF my $statement = $dbh->prepare($query); $statement->bind_param(1, $username, SQL_VARCHAR); $statement->bind_param(2, $username, SQL_VARCHAR); $statement->bind_param(3, $project_id, SQL_INTEGER); $statement->execute(); my $can_access = defined($statement->fetchrow_hashref()); $statement->finish(); undef $statement; return $can_access; } sub is_valid_user_database { my ($dbh, $username, $user_secret) = @_; my $query = << 'EOF'; SELECT unix_pw FROM user WHERE user_name=?; EOF my $statement = $dbh->prepare($query); $statement->bind_param(1, $username, SQL_VARCHAR); $statement->execute(); my ($row_secret) = $statement->fetchrow_array(); my $is_authenticated = compare_string_constant_time(crypt($user_secret, $row_secret), $row_secret); $statement->finish(); undef $statement; return $is_authenticated; } sub get_tuleap_username_from_ldap_uid { my($dbh, $username) = @_; my $query = << 'EOF'; SELECT user_name FROM user JOIN plugin_ldap_user ON plugin_ldap_user.user_id=user.user_id WHERE ldap_uid=? AND user.status IN ('A', 'R'); EOF my $statement = $dbh->prepare($query); $statement->bind_param(1, $username, SQL_VARCHAR); $statement->execute(); my ($tuleap_username) = $statement->fetchrow_array(); $statement->finish(); undef $statement; return $tuleap_username; } sub is_valid_user_ldap { my ($cfg, $username, $user_secret) = @_; my $ldap = connect_and_bind_ldap($cfg); if (! defined($ldap)) { return; } my $user_dn = get_user_dn($cfg, $ldap, $username); if (!defined($user_dn)) { return; } my $mesg = $ldap->bind($user_dn, password => $user_secret); $ldap->unbind(); return ! $mesg->code(); } sub hash_user_secret { my ($user_secret) = @_; return sha512($user_secret); } sub connect_and_bind_ldap { my ($cfg) = @_; my @servers = split(m/[,]/xms, $cfg->{TuleapLdapServers}); foreach my $server (@servers) { my $ldap = Net::LDAP->new($server, onerror => undef); if (defined($ldap) && ldap_bind($cfg, $ldap)) { return $ldap; } } return; } sub ldap_bind() { my ($cfg, $ldap) = @_; if ($cfg->{TuleapLdapBindDN}) { return $ldap->bind($cfg->{TuleapLdapBindDN}, password => $cfg->{TuleapLdapBindPassword}); } return $ldap->bind(); } sub get_user_dn() { my ($cfg, $ldap, $username) = @_; my $mesg = $ldap->search( base => $cfg->{TuleapLdapDN}, filter => $cfg->{TuleapLdapUid} . q/=/ . escape_filter_value($username), scope => 'sub' ); if (! defined($mesg)) { return; } my $entry = $mesg->shift_entry(); if ($entry) { return $entry->dn(); } return; } sub compare_string_constant_time { my ($string1, $string2) = @_; if (length($string1) != length($string2)) { return 0; } my $result = 0; for (0..length($string1)) { $result |= ord(substr($string1, $_, 1)) ^ ord(substr($string2, $_, 1)); } return $result == 0; } Apache2::Module::add(__PACKAGE__, \@directives); 1;