diff options
25 files changed, 1152 insertions, 484 deletions
diff --git a/Bugzilla.pm b/Bugzilla.pm index 366acb163..cded650d7 100644 --- a/Bugzilla.pm +++ b/Bugzilla.pm @@ -24,10 +24,13 @@ package Bugzilla; use strict; +use Bugzilla::Auth; use Bugzilla::CGI; use Bugzilla::Config; +use Bugzilla::Constants; use Bugzilla::DB; use Bugzilla::Template; +use Bugzilla::User; my $_template; sub template { @@ -43,6 +46,60 @@ sub cgi { return $_cgi; } +my $_user; +sub user { + my $class = shift; + return $_user; +} + +sub login { + my ($class, $type) = @_; + + # Avoid double-logins, which may confuse the auth code + # (double cookies, odd compat code settings, etc) + # This is particularly important given the munging for + # $::COOKIE{'Bugzilla_login'} from a userid to a loginname + # (for backwards compat) + if (defined $_user) { + return $_user->{id}; + } + + $type = LOGIN_NORMAL unless defined $type; + + # For now, we can only log in from a cgi + # One day, we'll be able to log in via apache auth, an email message's + # PGP signature, and so on + + use Bugzilla::Auth::CGI; + my $userid = Bugzilla::Auth::CGI->login($type); + if ($userid) { + $_user = new Bugzilla::User($userid); + + # Compat stuff + $::userid = $userid; + &::ConfirmGroup($userid); + + # Evil compat hack. The cookie stores the id now, not the name, but + # old code still looks at this to get the current user's email + # so it needs to be set. + $::COOKIE{'Bugzilla_login'} = $_user->{email}; + + $::vars->{'user'} = &::GetUserInfo($userid); + } else { + # Old compat stuff + + $::userid = 0; + delete $::COOKIE{'Bugzilla_login'}; + delete $::COOKIE{'Bugzilla_logincookie'}; + # NB - Can't delete from $cgi->cookie, so the cookie data will + # remain there + # People shouldn't rely on the cookie param for the username + # - use Bugzilla->user instead! + } + + return $userid || 0; +} + my $_dbh; my $_dbh_main; my $_dbh_shadow; @@ -93,6 +150,7 @@ sub switch_to_main_db { # Per process cleanup sub _cleanup { undef $_cgi; + undef $_user; # See bug 192531. If we don't clear the possibly active statement handles, # then when this is called from the END block, it happens _before_ the @@ -192,6 +250,16 @@ The current C<cgi> object. Note that modules should B<not> be using this in general. Not all Bugzilla actions are cgi requests. Its useful as a convenience method for those scripts/templates which are only use via CGI, though. +=item C<user> + +The current L<Bugzilla::User>. C<undef> if there is no currently logged in user +or if the login code has not yet been run. + +=item C<login> + +Logs in a user, returning the userid, or C<0> if there is no logged in user. +See L<Bugzilla::Auth>. + =item C<dbh> The current database handle. See L<DBI>. diff --git a/Bugzilla/Auth.pm b/Bugzilla/Auth.pm new file mode 100644 index 000000000..902ae0f05 --- /dev/null +++ b/Bugzilla/Auth.pm @@ -0,0 +1,214 @@ +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# The contents of this file are subject to the Mozilla Public +# License Version 1.1 (the "License"); you may not use this file +# except in compliance with the License. You may obtain a copy of +# the License at http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS +# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or +# implied. See the License for the specific language governing +# rights and limitations under the License. +# +# The Original Code is the Bugzilla Bug Tracking System. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are +# Copyright (C) 1998 Netscape Communications Corporation. All +# Rights Reserved. +# +# Contributor(s): Bradley Baetz <bbaetz@acm.org> + +package Bugzilla::Auth; + +use strict; + +use Bugzilla::Config; +use Bugzilla::Constants; + +# 'inherit' from the main loginmethod +BEGIN { + my $loginmethod = Param("loginmethod"); + require "Bugzilla/Auth/" . $loginmethod . ".pm"; + + our @ISA; + push (@ISA, "Bugzilla::Auth::" . $loginmethod); +} + +# PRIVATE + +# Returns the network address for a given ip +sub get_netaddr { + my $ipaddr = shift; + + # Check for a valid IPv4 addr which we know how to parse + if (!$ipaddr || $ipaddr !~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) { + return undef; + } + + my $addr = unpack("N", pack("CCCC", split(/\./, $ipaddr))); + + my $maskbits = Param('loginnetmask'); + + $addr >>= (32-$maskbits); + $addr <<= (32-$maskbits); + return join(".", unpack("CCCC", pack("N", $addr))); +} + +1; + +__END__ + +=head1 NAME + +Bugzilla::Auth - Authentication handling for Bugzilla users + +=head1 DESCRIPTION + +Handles authentication for Bugzilla users. + +Authentication from Bugzilla involves two sets of modules. One set is used to +obtain the data (from CGI, email, etc), and the other set uses this data to +authenticate against the datasource (the Bugzilla DB, LDAP, cookies, etc). + +The handlers for the various types of authentication (DB/LDAP/cookies/etc) +provide the actual code for each specific method of authentication. + +The source modules (currently, only L<Bugzilla::Auth::CGI|Bugzilla::Auth::CGI> +then use those methods to do the authentication. + +I<Bugzilla::Auth> itself inherits from the default authentication handler, +identified by the I<loginmethod> param. + +=head1 METHODS + +C<Bugzilla::Auth> contains several helper methods to be used by +authentication or login modules. + +=over 4 + +=item C<Bugzilla::Auth::get_netaddr($ipaddr)> + +Given an ip address, this returns the associated network address, using +C<Param('loginnetmask')> at the netmask. This can be used to obtain data in +order to restrict weak authentication methods (such as cookies) to only some +addresses. + +=back + +=head1 AUTHENTICATION + +Authentication modules check a users's credentials (username, password, etc) to +verify who the user is. + +=head2 METHODS + +=over 4 + +=item C<authenticate($username, $pass)> + +This method is passed a username and a password, and returns a list containing +up to four return values, depending on the results of the authentication. + +The first return value is one of the status codes defined in +L<Bugzilla::Constants|Bugzilla::Constants> and described below. The rest of +the return values are status code-specific and are explained in the status +code descriptions. + +=over 4 + +=item C<AUTH_OK> + +Authentication succeeded. The second variable is the userid of the new user. + +=item C<AUTH_NODATA> + +Insufficient login data was provided by the user. This may happen in several +cases, such as cookie authentication when the cookie is not present. + +=item C<AUTH_ERROR> + +An error occurred when trying to use the login mechanism. The second return +value may contain the Bugzilla userid, but will probably be C<undef>, +signifiying that the userid is unknown. The third value is a tag describing +the error used by the authentication error templates to print a description +to the user. The optional fourth argument is a hashref of values used as part +of the tag's error descriptions. + +This error template must have a name/location of +I<account/auth/C<lc(authentication-type)>-error.html.tmpl>. + +=item C<AUTH_LOGINFAILED> + +An incorrect username or password was given. Note that for security reasons, +both cases return the same error code. However, in the case of a valid +username, the second argument may be the userid. The authentication +mechanism may not always be able to discover the userid if the password is +not known, so whether or not this argument is present is implementation +specific. For security reasons, the presence or lack of a userid value should +not be communicated to the user. + +The third argument is an optional tag from the authentication server +describing the error. The tag can be used by a template to inform the user +about the error. Similar to C<AUTH_ERROR>, an optional hashref may be +present as a fourth argument, to be used by the tag to give more detailed +information. + +=item C<AUTH_DISABLED> + +The user successfully logged in, but their account has been disabled. The +second argument in the returned array is the userid, and the third is some +text explaining why the account was disabled. This text would typically come +from the C<disabledtext> field in the C<profiles> table. Note that this +argument is a string, not a tag. + +=back + +=item C<can_edit> + +This determines if the user's account details can be modified. If this +method returns a C<true> value, then accounts can be created and modified +through the Bugzilla user interface. Forgotten passwords can also be +retrieved through the L<Token interface|Token>. + +=back + +=head1 LOGINS + +A login module can be used to try to log in a Bugzilla user in a particular +way. For example, L<Bugzilla::Auth::CGI|Bugzilla::Auth::CGI> logs in users +from CGI scripts, first by trying database authentication against the +Bugzilla C<profiles> table, and then by trying cookies as a fallback. + +A login module consists of a single method, C<login>, which takes a C<$type> +argument, using constants found in C<Bugzilla::Constants>. + +=over 4 + +=item C<LOGIN_OPTIONAL> + +A login is never required to access this data. Attempting to login is still +useful, because this allows the page to be personalised. Note that an +incorrect login will still trigger an error, even though the lack of a login +will be OK. + +=item C<LOGIN_NORMAL> + +A login may or may not be required, depending on the setting of the +I<requirelogin> parameter. + +=item C<LOGIN_REQUIRED> + +A login is always required to access this data. + +=back + +The login module uses various authentication modules to try to authenticate +a user, and returns the userid on success, or C<undef> on failure. + +When a login is required, but data is not present, it is the job of the login +module to prompt the user for this data. + +=head1 SEE ALSO + +L<Bugzilla::Auth::CGI>, L<Bugzilla::Auth::Cookie>, L<Bugzilla::Auth::DB> diff --git a/Bugzilla/Auth/CGI.pm b/Bugzilla/Auth/CGI.pm new file mode 100644 index 000000000..b7c2e6c42 --- /dev/null +++ b/Bugzilla/Auth/CGI.pm @@ -0,0 +1,195 @@ +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# The contents of this file are subject to the Mozilla Public +# License Version 1.1 (the "License"); you may not use this file +# except in compliance with the License. You may obtain a copy of +# the License at http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS +# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or +# implied. See the License for the specific language governing +# rights and limitations under the License. +# +# The Original Code is the Bugzilla Bug Tracking System. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are +# Copyright (C) 1998 Netscape Communications Corporation. All +# Rights Reserved. +# +# Contributor(s): Terry Weissman <terry@mozilla.org> +# Dan Mosedale <dmose@mozilla.org> +# Joe Robins <jmrobins@tgix.com> +# Dave Miller <justdave@syndicomm.com> +# Christopher Aillon <christopher@aillon.com> +# Gervase Markham <gerv@gerv.net> +# Christian Reis <kiko@async.com.br> +# Bradley Baetz <bbaetz@acm.org> + +package Bugzilla::Auth::CGI; + +use strict; + +use Bugzilla::Config; +use Bugzilla::Constants; +use Bugzilla::Util; + +sub login { + my ($class, $type) = @_; + + # 'NORMAL' logins depend on the 'requirelogin' param + if ($type == LOGIN_NORMAL) { + $type = Param('requirelogin') ? LOGIN_REQUIRED : LOGIN_OPTIONAL; + } + + my $cgi = Bugzilla->cgi; + + # First, try the actual login method against form variables + my $username = $cgi->param("Bugzilla_login"); + my $passwd = $cgi->param("Bugzilla_password"); + + my $authmethod = Param("loginmethod"); + my ($authres, $userid, $extra, $info) = + Bugzilla::Auth->authenticate($username, $passwd); + + if ($authres == AUTH_OK) { + # Login via username/password was correct and valid, so create + # and send out the login cookies + my $ipaddr = $cgi->remote_addr; + unless ($cgi->param('Bugzilla_restrictlogin') || + Param('loginnetmask') == 32) { + $ipaddr = get_netaddr($ipaddr); + } + + # The IP address is valid, at least for comparing with itself in a + # subsequent login + trick_taint($ipaddr); + + my $dbh = Bugzilla->dbh; + $dbh->do("INSERT INTO logincookies (userid, ipaddr) VALUES (?, ?)", + undef, + $userid, $ipaddr); + my $logincookie = $dbh->selectrow_array("SELECT LAST_INSERT_ID()"); + my $cookiepath = Param("cookiepath"); + print "Set-Cookie: Bugzilla_login=$userid ; path=$cookiepath; expires=Sun, 30-Jun-2029 00:00:00 GMT\n"; + print "Set-Cookie: Bugzilla_logincookie=$logincookie ; path=$cookiepath; expires=Sun, 30-Jun-2029 00:00:00 GMT\n"; + + # compat code. The cookie value is used for logouts, and that + # isn't generic yet. + $::COOKIE{'Bugzilla_logincookie'} = $logincookie; + } elsif ($authres == AUTH_NODATA) { + # No data from the form, so try to login via cookies + $username = $cgi->cookie("Bugzilla_login"); + $passwd = $cgi->cookie("Bugzilla_logincookie"); + + require Bugzilla::Auth::Cookie; + my $authmethod = "Cookie"; + + ($authres, $userid, $extra) = + Bugzilla::Auth::Cookie->authenticate($username, $passwd); + + # If the data for the cookie was incorrect, then treat that as + # NODATA. This could occur if the user's IP changed, for example. + # Give them un-loggedin access if allowed (checked below) + $authres = AUTH_NODATA if $authres == AUTH_LOGINFAILED; + } + + # Now check the result + + # An error may have occurred with the login mechanism + if ($authres == AUTH_ERROR) { + $::vars->{'authmethod'} = lc($authmethod); + $::vars->{'userid'} = $userid; + $::vars->{'auth_err_tag'} = $extra; + $::vars->{'info'} = $info; + + &::ThrowCodeError("auth_err"); + } + + # We can load the page if the login was ok, or there was no data + # but a login wasn't required + if ($authres == AUTH_OK || + ($authres == AUTH_NODATA && $type == LOGIN_OPTIONAL)) { + + # login succeded, so we're done + return $userid; + } + + # No login details were given, but we require a login if the + # page does + if ($authres == AUTH_NODATA && $type == LOGIN_REQUIRED) { + # Throw up the login page + + print "Content-Type: text/html\n\n"; + + my $template = Bugzilla->template; + $template->process("account/auth/login.html.tmpl", + { 'target' => $cgi->url(-relative=>1), + 'form' => \%::FORM, + 'mform' => \%::MFORM, + 'caneditaccount' => Bugzilla::Auth->can_edit, + } + ) + || &::ThrowTemplateError($template->error()); + + # This seems like as good as time as any to get rid of old + # crufty junk in the logincookies table. Get rid of any entry + # that hasn't been used in a month. + Bugzilla->dbh->do("DELETE FROM logincookies " . + "WHERE TO_DAYS(NOW()) - TO_DAYS(lastused) > 30"); + + exit; + } + + # The username/password may be wrong + # Don't let the user know whether the username exists or whether + # the password was just wrong. (This makes it harder for a cracker + # to find account names by brute force) + if ($authres == AUTH_LOGINFAILED) { + &::ThrowUserError("invalid_username_or_password"); + } + + # The account may be disabled + if ($authres == AUTH_DISABLED) { + # Clear the cookie + my $cookiepath = Param("cookiepath"); + print "Set-Cookie: Bugzilla_login= ; path=$cookiepath; expires=Sun, 30-Jun-80 00:00:00 GMT\n"; + print "Set-Cookie: Bugzilla_logincookie= ; path=$cookiepath; expires=Sun, 30-Jun-80 00:00:00 GMT\n"; + # and throw a user error + &::ThrowUserError("account_disabled", + {'disabled_reason' => $extra}); + } + + # If we get here, then we've run out of options, which shouldn't happen + &::ThrowCodeError("authres_unhandled", + { authres => $authres, + type => $type, + } + ); + +} + +1; + +__END__ + +=head1 NAME + +Bugzilla::Auth::CGI - CGI-based logins for Bugzilla + +=head1 SUMMARY + +This is a L<login module|Bugzilla::Auth/"LOGIN"> for Bugzilla. Users connecting +from a CGI script use this module to authenticate. + +=head1 BEHAVIOUR + +Users are first authenticated against the default authentication handler, +using the CGI parameters I<Bugzilla_login> and I<Bugzilla_password>. + +If no data is present for that, then cookies are tried, using +L<Bugzilla::Auth::Cookie>. + +=head1 SEE ALSO + +L<Bugzilla::Auth> diff --git a/Bugzilla/Auth/Cookie.pm b/Bugzilla/Auth/Cookie.pm new file mode 100644 index 000000000..7dd2967fb --- /dev/null +++ b/Bugzilla/Auth/Cookie.pm @@ -0,0 +1,119 @@ +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# The contents of this file are subject to the Mozilla Public +# License Version 1.1 (the "License"); you may not use this file +# except in compliance with the License. You may obtain a copy of +# the License at http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS +# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or +# implied. See the License for the specific language governing +# rights and limitations under the License. +# +# The Original Code is the Bugzilla Bug Tracking System. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are +# Copyright (C) 1998 Netscape Communications Corporation. All +# Rights Reserved. +# +# Contributor(s): Terry Weissman <terry@mozilla.org> +# Dan Mosedale <dmose@mozilla.org> +# Joe Robins <jmrobins@tgix.com> +# Dave Miller <justdave@syndicomm.com> +# Christopher Aillon <christopher@aillon.com> +# Gervase Markham <gerv@gerv.net> +# Christian Reis <kiko@async.com.br> +# Bradley Baetz <bbaetz@acm.org> + +package Bugzilla::Auth::Cookie; + +use strict; + +use Bugzilla::Auth; +use Bugzilla::Config; +use Bugzilla::Constants; +use Bugzilla::Util; + +sub authenticate { + my ($class, $login, $login_cookie) = @_; + + return (AUTH_NODATA) unless defined $login && defined $login_cookie; + + my $cgi = Bugzilla->cgi; + + my $ipaddr = $cgi->remote_addr(); + my $netaddr = Bugzilla::Auth::get_netaddr($ipaddr); + + # Anything goes for these params - they're just strings which + # we're going to verify against the db + trick_taint($login); + trick_taint($login_cookie); + trick_taint($ipaddr); + + my $query = "SELECT profiles.userid, profiles.disabledtext " . + "FROM logincookies, profiles " . + "WHERE logincookies.cookie=? AND " . + " logincookies.userid=profiles.userid AND " . + " logincookies.userid=? AND " . + " (logincookies.ipaddr=?"; + if (defined $netaddr) { + trick_taint($netaddr); + $query .= " OR logincookies.ipaddr=?"; + } + $query .= ")"; + + my $dbh = Bugzilla->dbh; + my ($userid, $disabledtext) = $dbh->selectrow_array($query, undef, + $login_cookie, + $login, + $ipaddr, + $netaddr); + + return (AUTH_DISABLED, $userid, $disabledtext) + if ($disabledtext); + + if ($userid) { + # If we logged in successfully, then update the lastused time on the + # login cookie + $dbh->do("UPDATE logincookies SET lastused=NULL WHERE cookie=?", + undef, + $login_cookie); + + # compat code. The cookie value is used for logouts, and that + # isn't generic yet. Detaint it so that its usable + detaint_natural($::COOKIE{'Bugzilla_logincookie'}); + + return (AUTH_OK, $userid); + } + + # If we get here, then the login failed. + return (AUTH_LOGINFAILED); +} + +1; + +__END__ + +=head1 NAME + +Bugzilla::Cookie - cookie authentication for Bugzilla + +=head1 SUMMARY + +This is an L<authentication module|Bugzilla::Auth/"AUTHENTICATION"> for +Bugzilla, which logs the user in using a persistent cookie stored in the +C<logincookies> table. + +The actual password is not stored in the cookie; only the userid and a +I<logincookie> (which is used to reverify the login without requiring the +password to be sent over the network) are. These I<logincookies> are +restricted to certain IP addresses as a security meaure. The exact +restriction can be specified by the admin via the C<loginnetmask> parameter. + +This module does not ever send a cookie (It has no way of knowing when a user +is successfully logged in). Instead L<Bugzilla::Auth::CGI> handles this. + +=head1 SEE ALSO + +L<Bugzilla::Auth>, L<Bugzilla::Auth::CGI> diff --git a/Bugzilla/Auth/DB.pm b/Bugzilla/Auth/DB.pm new file mode 100644 index 000000000..55e4bc7c0 --- /dev/null +++ b/Bugzilla/Auth/DB.pm @@ -0,0 +1,102 @@ +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# The contents of this file are subject to the Mozilla Public +# License Version 1.1 (the "License"); you may not use this file +# except in compliance with the License. You may obtain a copy of +# the License at http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS +# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or +# implied. See the License for the specific language governing +# rights and limitations under the License. +# +# The Original Code is the Bugzilla Bug Tracking System. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are +# Copyright (C) 1998 Netscape Communications Corporation. All +# Rights Reserved. +# +# Contributor(s): Terry Weissman <terry@mozilla.org> +# Dan Mosedale <dmose@mozilla.org> +# Joe Robins <jmrobins@tgix.com> +# Dave Miller <justdave@syndicomm.com> +# Christopher Aillon <christopher@aillon.com> +# Gervase Markham <gerv@gerv.net> +# Christian Reis <kiko@async.com.br> +# Bradley Baetz <bbaetz@acm.org> + +package Bugzilla::Auth::DB; + +use strict; + +use Bugzilla::Config; +use Bugzilla::Constants; +use Bugzilla::Util; + +sub authenticate { + my ($class, $username, $passwd) = @_; + + return (AUTH_NODATA) unless defined $username && defined $passwd; + + my $dbh = Bugzilla->dbh; + + # We're just testing against the db, so any value is ok + trick_taint($username); + + # Retrieve the user's ID and crypted password from the database. + my $sth = $dbh->prepare_cached("SELECT userid,cryptpassword,disabledtext " . + "FROM profiles " . + "WHERE login_name=?"); + my ($userid, $realcryptpwd, $disabledtext) = + $dbh->selectrow_array($sth, + undef, + $username); + + # If the user doesn't exist, return now + return (AUTH_LOGINFAILED) unless defined $userid; + + # OK, now authenticate the user + + # Get the salt from the user's crypted password. + my $salt = $realcryptpwd; + + # Using the salt, crypt the password the user entered. + my $enteredCryptedPassword = crypt($passwd, $salt); + + # Make sure the passwords match or return an error + return (AUTH_LOGINFAILED, $userid) unless + ($enteredCryptedPassword eq $realcryptpwd); + + # Now we know that the user has logged in successfully, + # so delete any password tokens for them + require Token; + Token::DeletePasswordTokens("user logged in"); + + # The user may have had their account disabled + return (AUTH_DISABLED, $userid, $disabledtext) + if $disabledtext ne ''; + + # If we get to here, then the user is allowed to login, so we're done! + return (AUTH_OK, $userid); +} + +sub can_edit { return 1; } + +1; + +__END__ + +=head1 NAME + +Bugzilla::DB - database authentication for Bugzilla + +=head1 SUMMARY + +This is an L<authentication module|Bugzilla::Auth/"AUTHENTICATION"> for +Bugzilla, which logs the user in using the password stored in the C<profiles> +table. This is the most commonly used authentication module. + +=head1 SEE ALSO + +L<Bugzilla::Auth> diff --git a/Bugzilla/Auth/LDAP.pm b/Bugzilla/Auth/LDAP.pm new file mode 100644 index 000000000..4570bdde9 --- /dev/null +++ b/Bugzilla/Auth/LDAP.pm @@ -0,0 +1,185 @@ +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# The contents of this file are subject to the Mozilla Public +# License Version 1.1 (the "License"); you may not use this file +# except in compliance with the License. You may obtain a copy of +# the License at http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS +# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or +# implied. See the License for the specific language governing +# rights and limitations under the License. +# +# The Original Code is the Bugzilla Bug Tracking System. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are +# Copyright (C) 1998 Netscape Communications Corporation. All +# Rights Reserved. +# +# Contributor(s): Terry Weissman <terry@mozilla.org> +# Dan Mosedale <dmose@mozilla.org> +# Joe Robins <jmrobins@tgix.com> +# Dave Miller <justdave@syndicomm.com> +# Christopher Aillon <christopher@aillon.com> +# Gervase Markham <gerv@gerv.net> +# Christian Reis <kiko@async.com.br> +# Bradley Baetz <bbaetz@acm.org> + +package Bugzilla::Auth::LDAP; + +use strict; + +use Bugzilla::Config; +use Bugzilla::Constants; + +use Net::LDAP; + +sub authenticate { + my ($class, $username, $passwd) = @_; + + # If no password was provided, then fail the authentication. + # While it may be valid to not have an LDAP password, when you + # bind without a password (regardless of the binddn value), you + # will get an anonymous bind. I do not know of a way to determine + # whether a bind is anonymous or not without making changes to the + # LDAP access control settings + return (AUTH_NODATA) unless $username && $passwd; + + # We need to bind anonymously to the LDAP server. This is + # because we need to get the Distinguished Name of the user trying + # to log in. Some servers (such as iPlanet) allow you to have unique + # uids spread out over a subtree of an area (such as "People"), so + # just appending the Base DN to the uid isn't sufficient to get the + # user's DN. For servers which don't work this way, there will still + # be no harm done. + my $LDAPserver = Param("LDAPserver"); + if ($LDAPserver eq "") { + return (AUTH_ERROR, undef, "server_not_defined"); + } + + my $LDAPport = "389"; # default LDAP port + if($LDAPserver =~ /:/) { + ($LDAPserver, $LDAPport) = split(":",$LDAPserver); + } + my $LDAPconn = Net::LDAP->new($LDAPserver, port => $LDAPport, version => 3); + if(!$LDAPconn) { + return (AUTH_ERROR, undef, "connect_failed"); + } + + my $mesg; + if (Param("LDAPbinddn")) { + my ($LDAPbinddn,$LDAPbindpass) = split(":",Param("LDAPbinddn")); + $mesg = $LDAPconn->bind($LDAPbinddn, password => $LDAPbindpass); + } + else { + $mesg = $LDAPconn->bind(); + } + if($mesg->code) { + return (AUTH_ERROR, undef, + "connect_failed", + { errstr => $mesg->err }); + } + + # We've got our anonymous bind; let's look up this user. + $mesg = $LDAPconn->search( base => Param("LDAPBaseDN"), + scope => "sub", + filter => Param("LDAPuidattribute") . "=$username", + attrs => ['dn'], + ); + return (AUTH_LOGINFAILED, undef, "lookup_failure") + unless $mesg->count; + + # Now we get the DN from this search. + my $userDN = $mesg->shift_entry->dn; + + # Now we attempt to bind as the specified user. + $mesg = $LDAPconn->bind( $userDN, password => $passwd); + + return (AUTH_LOGINFAILED) if $mesg->code; + + # And now we're going to repeat the search, so that we can get the + # mail attribute for this user. + $mesg = $LDAPconn->search( base => Param("LDAPBaseDN"), + scope => "sub", + filter => Param("LDAPuidattribute") . "=$username", + ); + my $user_entry = $mesg->shift_entry if !$mesg->code && $mesg->count; + if(!$user_entry || !$user_entry->exists(Param("LDAPmailattribute"))) { + return (AUTH_ERROR, undef, + "cannot_retreive_attr", + { attr => Param("LDAPmailattribute") }); + } + + # get the mail attribute + $username = $user_entry->get_value(Param("LDAPmailattribute")); + # OK, so now we know that the user is valid. Lets try finding them in the + # Bugzilla database + + # XXX - should this part be made more generic, and placed in + # Bugzilla::Auth? Lots of login mechanisms may have to do this, although + # until we actually get some more, its hard to know - BB + + my $dbh = Bugzilla->dbh; + my $sth = $dbh->prepare_cached("SELECT userid, disabledtext " . + "FROM profiles " . + "WHERE login_name=?"); + my ($userid, $disabledtext) = + $dbh->selectrow_array($sth, + undef, + $username); + + # If the user doesn't exist, then they need to be added + unless ($userid) { + # We'll want the user's name for this. + my $userRealName = $user_entry->get_value("displayName"); + if($userRealName eq "") { + $userRealName = $user_entry->get_value("cn"); + } + &::InsertNewUser($username, $userRealName); + + my ($userid, $disabledtext) = $dbh->selectrow_array($sth, + undef, + $username); + return (AUTH_ERROR, $userid, "no_userid") + unless $userid; + } + + # we're done, so disconnect + $LDAPconn->unbind; + + # Test for disabled account + return (AUTH_DISABLED, $userid, $disabledtext) + if $disabledtext ne ''; + + # If we get to here, then the user is allowed to login, so we're done! + return (AUTH_OK, $userid); +} + +sub can_edit { return 0; } + +1; + +__END__ + +=head1 NAME + +Bugzilla::Auth::LDAP - LDAP based authentication for Bugzilla + +This is an L<authentication module|Bugzilla::Auth/"AUTHENTICATION"> for +Bugzilla, which logs the user in using an LDAP directory. + +=head1 DISCLAIMER + +B<This module is experimental>. It is poorly documented, and not very flexible. +Search L<http://bugzilla.mozilla.org/> for a list of known LDAP bugs. + +None of the core Bugzilla developers, nor any of the large installations, use +this module, and so it has received less testing. (In fact, this iteration +hasn't been tested at all) + +Patches are accepted. + +=head1 SEE ALSO + +L<Bugzilla::Auth> diff --git a/Bugzilla/Config.pm b/Bugzilla/Config.pm index 6a34396be..c1f3e9103 100644 --- a/Bugzilla/Config.pm +++ b/Bugzilla/Config.pm @@ -186,6 +186,11 @@ sub UpdateParams { $param{'useentrygroupdefault'} = $param{'usebuggroupsentry'}; } + # Modularise auth code + if (exists $param{'useLDAP'} && !exists $param{'loginmethod'}) { + $param{'loginmethod'} = $param{'useLDAP'} ? "LDAP" : "DB"; + } + # --- DEFAULTS FOR NEW PARAMS --- foreach my $item (@param_list) { diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm index 70773e036..5e6b5365d 100644 --- a/Bugzilla/Constants.pm +++ b/Bugzilla/Constants.pm @@ -36,7 +36,17 @@ use base qw(Exporter); CONTROLMAPSHOWN CONTROLMAPDEFAULT CONTROLMAPMANDATORY - ); + + AUTH_OK + AUTH_NODATA + AUTH_ERROR + AUTH_LOGINFAILED + AUTH_DISABLED + + LOGIN_OPTIONAL + LOGIN_NORMAL + LOGIN_REQUIRED +); # CONSTANTS @@ -72,5 +82,16 @@ use constant CONTROLMAPSHOWN => 1; use constant CONTROLMAPDEFAULT => 2; use constant CONTROLMAPMANDATORY => 3; -1; +# See Bugzilla::Auth for docs for these +use constant AUTH_OK => 0; +use constant AUTH_NODATA => 1; +use constant AUTH_ERROR => 2; +use constant AUTH_LOGINFAILED => 3; +use constant AUTH_DISABLED => 4; + +use constant LOGIN_OPTIONAL => 0; +use constant LOGIN_NORMAL => 1; +use constant LOGIN_REQUIRED => 2; + +1; diff --git a/Bugzilla/DB.pm b/Bugzilla/DB.pm index 29935928d..1d2e96614 100644 --- a/Bugzilla/DB.pm +++ b/Bugzilla/DB.pm @@ -61,8 +61,6 @@ our @SQLStateStack = (); sub SendSQL { my ($str) = @_; - require Bugzilla; - $_current_sth = Bugzilla->dbh->prepare($str); $_current_sth->execute; @@ -79,8 +77,6 @@ sub SqlQuote { # Backwards compat code return "''" if not defined $str; - require Bugzilla; - my $res = Bugzilla->dbh->quote($str); trick_taint($res); @@ -156,6 +152,7 @@ sub _connect { $db_pass, { RaiseError => 1, PrintError => 0, + ShowErrorStatement => 1, HandleError => \&_handle_error, FetchHashKeyName => 'NAME_lc', TaintIn => 1, diff --git a/Bugzilla/Token.pm b/Bugzilla/Token.pm index c8132b804..97d2da41d 100644 --- a/Bugzilla/Token.pm +++ b/Bugzilla/Token.pm @@ -237,16 +237,17 @@ sub Cancel { &::SendSQL("UNLOCK TABLES"); } -sub HasPasswordToken { - # Returns a password token if the user has one. - - my ($userid) = @_; - - &::SendSQL("SELECT token FROM tokens - WHERE userid = $userid AND tokentype = 'password' LIMIT 1"); - my ($token) = &::FetchSQLData(); - - return $token; +sub DeletePasswordTokens { + my ($userid, $reason) = @_; + + my $dbh = Bugzilla->dbh; + my $sth = $dbh->prepare("SELECT token " . + "FROM tokens " . + "WHERE userid=? AND tokentype='password'"); + $sth->execute($userid); + while (my $token = $sth->fetchrow_array) { + Token::Cancel($token, "user_logged_in"); + } } sub HasEmailChangeToken { @@ -34,12 +34,7 @@ use lib "."; use Bugzilla::Util; use Bugzilla::Config; - -# commented out the following snippet of code. this tosses errors into the -# CGI if you are perl 5.6, and doesn't if you have perl 5.003. -# We want to check for the existence of the LDAP modules here. -# eval "use Mozilla::LDAP::Conn"; -# my $have_ldap = $@ ? 0 : 1; +use Bugzilla::Constants; # Shut up misguided -w warnings about "used only once". For some reason, # "use vars" chokes on me when I try it here. @@ -202,82 +197,8 @@ sub PasswordForLogin { return $result; } -sub get_netaddr { - my ($ipaddr) = @_; - - # Check for a valid IPv4 addr which we know how to parse - if (!$ipaddr || $ipaddr !~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) { - return undef; - } - - my $addr = unpack("N", pack("CCCC", split(/\./, $ipaddr))); - - my $maskbits = Param('loginnetmask'); - - $addr >>= (32-$maskbits); - $addr <<= (32-$maskbits); - return join(".", unpack("CCCC", pack("N", $addr))); -} - -my $login_cookie_set = 0; -# If quietly_check_login is called with no arguments and logins are -# required, it will prompt for a login. sub quietly_check_login { - if (Param('requirelogin') && !(@_)) { - return confirm_login(); - } - $::disabledreason = ''; - my $userid = 0; - my $ipaddr = $ENV{'REMOTE_ADDR'}; - my $netaddr = get_netaddr($ipaddr); - if (defined $::COOKIE{"Bugzilla_login"} && - defined $::COOKIE{"Bugzilla_logincookie"}) { - my $query = "SELECT profiles.userid," . - " profiles.login_name, " . - " profiles.disabledtext " . - " FROM profiles, logincookies WHERE logincookies.cookie = " . - SqlQuote($::COOKIE{"Bugzilla_logincookie"}) . - " AND profiles.userid = logincookies.userid AND" . - " profiles.login_name = " . - SqlQuote($::COOKIE{"Bugzilla_login"}) . - " AND (logincookies.ipaddr = " . - SqlQuote($ipaddr); - if (defined $netaddr) { - $query .= " OR logincookies.ipaddr = " . SqlQuote($netaddr); - } - $query .= ")"; - SendSQL($query); - - my @row; - if (MoreSQLData()) { - ($userid, my $loginname, my $disabledtext) = FetchSQLData(); - if ($userid > 0) { - if ($disabledtext eq '') { - $::COOKIE{"Bugzilla_login"} = $loginname; # Makes sure case - # is in - # canonical form. - # We've just verified that this is ok - detaint_natural($::COOKIE{"Bugzilla_logincookie"}); - } else { - $::disabledreason = $disabledtext; - $userid = 0; - } - } - } - } - # if 'who' is passed in, verify that it's a good value - if ($::FORM{'who'}) { - my $whoid = DBname_to_id($::FORM{'who'}); - delete $::FORM{'who'} unless $whoid; - } - if (!$userid) { - delete $::COOKIE{"Bugzilla_login"}; - } - - $::userid = $userid; - ConfirmGroup($userid); - $vars->{'user'} = GetUserInfo($::userid); - return $userid; + return Bugzilla->login($_[0] ? LOGIN_OPTIONAL : LOGIN_NORMAL); } # Populate a hash with information about this user. @@ -351,281 +272,7 @@ sub MailPassword { } sub confirm_login { - my ($nexturl) = (@_); - -# Uncommenting the next line can help debugging... -# print "Content-type: text/plain\n\n"; - - # I'm going to reorganize some of this stuff a bit. Since we're adding - # a second possible validation method (LDAP), we need to move some of this - # to a later section. -Joe Robins, 8/3/00 - my $enteredlogin = ""; - my $realcryptpwd = ""; - my $userid; - - # If the form contains Bugzilla login and password fields, use Bugzilla's - # built-in authentication to authenticate the user (otherwise use LDAP below). - if (defined $::FORM{"Bugzilla_login"} && defined $::FORM{"Bugzilla_password"}) { - # Make sure the user's login name is a valid email address. - $enteredlogin = $::FORM{"Bugzilla_login"}; - CheckEmailSyntax($enteredlogin); - - # Retrieve the user's ID and crypted password from the database. - SendSQL("SELECT userid, cryptpassword FROM profiles - WHERE login_name = " . SqlQuote($enteredlogin)); - ($userid, $realcryptpwd) = FetchSQLData(); - - # Make sure the user exists or throw an error (but do not admit it was a username - # error to make it harder for a cracker to find account names by brute force). - $userid || ThrowUserError("invalid_username_or_password"); - - # If this is a new user, generate a password, insert a record - # into the database, and email their password to them. - if ( defined $::FORM{"PleaseMailAPassword"} && !$userid ) { - # Ensure the new login is valid - if(!ValidateNewUser($enteredlogin)) { - ThrowUserError("account_exists"); - } - - my $password = InsertNewUser($enteredlogin, ""); - MailPassword($enteredlogin, $password); - - $vars->{'login'} = $enteredlogin; - - print "Content-Type: text/html\n\n"; - $template->process("account/created.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - } - - # Otherwise, authenticate the user. - else { - # Get the salt from the user's crypted password. - my $salt = $realcryptpwd; - - # Using the salt, crypt the password the user entered. - my $enteredCryptedPassword = crypt( $::FORM{"Bugzilla_password"} , $salt ); - - # Make sure the passwords match or throw an error. - ($enteredCryptedPassword eq $realcryptpwd) - || ThrowUserError("invalid_username_or_password"); - - # If the user has successfully logged in, delete any password tokens - # lying around in the system for them. - use Token; - my $token = Token::HasPasswordToken($userid); - while ( $token ) { - Token::Cancel($token, 'user_logged_in'); - $token = Token::HasPasswordToken($userid); - } - } - - } elsif (Param("useLDAP") && - defined $::FORM{"LDAP_login"} && - defined $::FORM{"LDAP_password"}) { - # If we're using LDAP for login, we've got an entirely different - # set of things to check. - -# see comment at top of file near eval - # First, if we don't have the LDAP modules available to us, we can't - # do this. -# if(!$have_ldap) { -# print "Content-type: text/html\n\n"; -# PutHeader("LDAP not enabled"); -# print "The necessary modules for LDAP login are not installed on "; -# print "this machine. Please send mail to ".Param("maintainer"); -# print " and notify him of this problem.\n"; -# PutFooter(); -# exit; -# } - - # Next, we need to bind anonymously to the LDAP server. This is - # because we need to get the Distinguished Name of the user trying - # to log in. Some servers (such as iPlanet) allow you to have unique - # uids spread out over a subtree of an area (such as "People"), so - # just appending the Base DN to the uid isn't sufficient to get the - # user's DN. For servers which don't work this way, there will still - # be no harm done. - my $LDAPserver = Param("LDAPserver"); - if ($LDAPserver eq "") { - print "Content-type: text/html\n\n"; - PutHeader("LDAP server not defined"); - print "The LDAP server for authentication has not been defined. "; - print "Please contact ".Param("maintainer")." "; - print "and notify him of this problem.\n"; - PutFooter(); - exit; - } - - my $LDAPport = "389"; #default LDAP port - if($LDAPserver =~ /:/) { - ($LDAPserver, $LDAPport) = split(":",$LDAPserver); - } - my $LDAPconn = new Mozilla::LDAP::Conn($LDAPserver,$LDAPport); - if(!$LDAPconn) { - print "Content-type: text/html\n\n"; - PutHeader("Unable to connect to LDAP server"); - print "I was unable to connect to the LDAP server for user "; - print "authentication. Please contact ".Param("maintainer"); - print " and notify him of this problem.\n"; - PutFooter(); - exit; - } - - # if no password was provided, then fail the authentication - # while it may be valid to not have an LDAP password, when you - # bind without a password (regardless of the binddn value), you - # will get an anonymous bind. I do not know of a way to determine - # whether a bind is anonymous or not without making changes to the - # LDAP access control settings - if ( ! $::FORM{"LDAP_password"} ) { - print "Content-type: text/html\n\n"; - PutHeader("Login Failed"); - print "You did not provide a password.\n"; - print "Please click <b>Back</b> and try again.\n"; - PutFooter(); - exit; - } - - # We've got our anonymous bind; let's look up this user. - my $dnEntry = $LDAPconn->search(Param("LDAPBaseDN"),"subtree","uid=".$::FORM{"LDAP_login"}); - if(!$dnEntry) { - print "Content-type: text/html\n\n"; - PutHeader("Login Failed"); - print "The username or password you entered is not valid.\n"; - print "Please click <b>Back</b> and try again.\n"; - PutFooter(); - exit; - } - - # Now we get the DN from this search. Once we've got that, we're - # done with the anonymous bind, so we close it. - my $userDN = $dnEntry->getDN; - $LDAPconn->close; - - # Now we attempt to bind as the specified user. - $LDAPconn = new Mozilla::LDAP::Conn($LDAPserver,$LDAPport,$userDN,$::FORM{"LDAP_password"}); - if(!$LDAPconn) { - print "Content-type: text/html\n\n"; - PutHeader("Login Failed"); - print "The username or password you entered is not valid.\n"; - print "Please click <b>Back</b> and try again.\n"; - PutFooter(); - exit; - } - - # And now we're going to repeat the search, so that we can get the - # mail attribute for this user. - my $userEntry = $LDAPconn->search(Param("LDAPBaseDN"),"subtree","uid=".$::FORM{"LDAP_login"}); - if(!$userEntry->exists(Param("LDAPmailattribute"))) { - print "Content-type: text/html\n\n"; - PutHeader("LDAP authentication error"); - print "I was unable to retrieve the ".Param("LDAPmailattribute"); - print " attribute from the LDAP server. Please contact "; - print Param("maintainer")." and notify him of this error.\n"; - PutFooter(); - exit; - } - - # Mozilla::LDAP::Entry->getValues returns an array for the attribute - # requested, even if there's only one entry. - $enteredlogin = ($userEntry->getValues(Param("LDAPmailattribute")))[0]; - - # We're going to need the cryptpwd for this user from the database - # so that we can set the cookie below, even though we're not going - # to use it for authentication. - $realcryptpwd = PasswordForLogin($enteredlogin); - - # If we don't get a result, then we've got a user who isn't in - # Bugzilla's database yet, so we've got to add them. - if($realcryptpwd eq "") { - # We'll want the user's name for this. - my $userRealName = ($userEntry->getValues("displayName"))[0]; - if($userRealName eq "") { - $userRealName = ($userEntry->getValues("cn"))[0]; - } - InsertNewUser($enteredlogin, $userRealName); - $realcryptpwd = PasswordForLogin($enteredlogin); - } - } # end LDAP authentication - - # And now, if we've logged in via either method, then we need to set - # the cookies. - if($enteredlogin ne "") { - $::COOKIE{"Bugzilla_login"} = $enteredlogin; - my $ipaddr = $ENV{'REMOTE_ADDR'}; - - # Unless we're restricting the login, or restricting would have no - # effect, loosen the IP which we record in the table - unless ($::FORM{'Bugzilla_restrictlogin'} || - Param('loginnetmask') == 32) { - $ipaddr = get_netaddr($ipaddr); - $ipaddr = $ENV{'REMOTE_ADDR'} unless defined $ipaddr; - } - SendSQL("insert into logincookies (userid,ipaddr) values (@{[DBNameToIdAndCheck($enteredlogin)]}, @{[SqlQuote($ipaddr)]})"); - SendSQL("select LAST_INSERT_ID()"); - my $logincookie = FetchOneColumn(); - - $::COOKIE{"Bugzilla_logincookie"} = $logincookie; - my $cookiepath = Param("cookiepath"); - if ($login_cookie_set == 0) { - $login_cookie_set = 1; - print "Set-Cookie: Bugzilla_login= " . url_quote($enteredlogin) . " ; path=$cookiepath; expires=Sun, 30-Jun-2029 00:00:00 GMT\n"; - print "Set-Cookie: Bugzilla_logincookie=$logincookie ; path=$cookiepath; expires=Sun, 30-Jun-2029 00:00:00 GMT\n"; - } - } - - # If anonymous logins are disabled, quietly_check_login will force - # the user to log in by calling confirm_login() when called by any - # code that does not call it with an argument. When confirm_login - # calls quietly_check_login, it must not result in confirm_login - # being called back. - $userid = quietly_check_login('do_not_recurse_here'); - - if (!$userid) { - if ($::disabledreason) { - my $cookiepath = Param("cookiepath"); - print "Set-Cookie: Bugzilla_login= ; path=$cookiepath; expires=Sun, 30-Jun-80 00:00:00 GMT -Set-Cookie: Bugzilla_logincookie= ; path=$cookiepath; expires=Sun, 30-Jun-80 00:00:00 GMT -Content-type: text/html - -"; - $vars->{'disabled_reason'} = $::disabledreason; - ThrowUserError("account_disabled"); - } - - if (!defined $nexturl || $nexturl eq "") { - # Sets nexturl to be argv0, stripping everything up to and - # including the last slash (or backslash on Windows). - $0 =~ m:([^/\\]*)$:; - $nexturl = $1; - } - - $vars->{'target'} = $nexturl; - $vars->{'form'} = \%::FORM; - $vars->{'mform'} = \%::MFORM; - - print "Content-type: text/html\n\n"; - $template->process("account/login.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - - # This seems like as good as time as any to get rid of old - # crufty junk in the logincookies table. Get rid of any entry - # that hasn't been used in a month. - if (Bugzilla->dbwritesallowed) { - SendSQL("DELETE FROM logincookies " . - "WHERE TO_DAYS(NOW()) - TO_DAYS(lastused) > 30"); - } - - exit; - } - - # Update the timestamp on our logincookie, so it'll keep on working. - if (Bugzilla->dbwritesallowed) { - SendSQL("UPDATE logincookies SET lastused = null " . - "WHERE cookie = $::COOKIE{'Bugzilla_logincookie'}"); - } - ConfirmGroup($userid); - return $userid; + return Bugzilla->login(LOGIN_REQUIRED); } sub PutHeader { @@ -659,15 +306,20 @@ sub ThrowCodeError { ($vars->{'error'}, my $extra_vars, my $unlock_tables) = (@_); SendSQL("UNLOCK TABLES") if $unlock_tables; - - # Copy the extra_vars into the vars hash - foreach my $var (keys %$extra_vars) { - $vars->{$var} = $extra_vars->{$var}; + + # If we don't have this test here, then the %@extra_vars vivifies + # the hashref, and then setting $vars->{'variables'} uses an empty hashref + # so the error template prints out a bogus header for the empty hash + if (defined $extra_vars) { + # Copy the extra_vars into the vars hash + foreach my $var (keys %$extra_vars) { + $vars->{$var} = $extra_vars->{$var}; + } + + # We may one day log something to file here also. + $vars->{'variables'} = $extra_vars; } - # We may one day log something to file here also. - $vars->{'variables'} = $extra_vars; - print "Content-type: text/html\n\n" if !$vars->{'header_done'}; $template->process("global/code-error.html.tmpl", $vars) || ThrowTemplateError($template->error()); @@ -237,16 +237,17 @@ sub Cancel { &::SendSQL("UNLOCK TABLES"); } -sub HasPasswordToken { - # Returns a password token if the user has one. - - my ($userid) = @_; - - &::SendSQL("SELECT token FROM tokens - WHERE userid = $userid AND tokentype = 'password' LIMIT 1"); - my ($token) = &::FetchSQLData(); - - return $token; +sub DeletePasswordTokens { + my ($userid, $reason) = @_; + + my $dbh = Bugzilla->dbh; + my $sth = $dbh->prepare("SELECT token " . + "FROM tokens " . + "WHERE userid=? AND tokentype='password'"); + $sth->execute($userid); + while (my $token = $sth->fetchrow_array) { + Token::Cancel($token, "user_logged_in"); + } } sub HasEmailChangeToken { diff --git a/checksetup.pl b/checksetup.pl index 6c3fc57b7..a8ea7b1e1 100755 --- a/checksetup.pl +++ b/checksetup.pl @@ -675,28 +675,6 @@ EOF } ########################################################################### -# Global Utility Library -########################################################################### - -# globals.pl clears the PATH, but File::Find uses Cwd::cwd() instead of -# Cwd::getcwd(), which we need to do because `pwd` isn't in the path - see -# http://www.xray.mpe.mpg.de/mailing-lists/perl5-porters/2001-09/msg00115.html -# As a workaround, since we only use File::Find in checksetup, which doesn't -# run in taint mode anyway, preserve the path... -my $origPath = $::ENV{'PATH'}; - -# Use the Bugzilla utility library for various functions. We do this -# here rather than at the top of the file so globals.pl doesn't define -# localconfig variables for us before we get a chance to check for -# their existence and create them if they don't exist. Also, globals.pl -# removes $ENV{'path'}, which we need in order to run `which mysql` above. -require "globals.pl"; - -# ...and restore it. This doesn't change tainting, so this will still cause -# errors if this script ever does run with -T. -$::ENV{'PATH'} = $origPath; - -########################################################################### # Check data directory ########################################################################### @@ -1215,6 +1193,33 @@ if ($my_webservergroup) { chmod 01777, 'graphs'; } +########################################################################### +# Global Utility Library +########################################################################### + +# This is done here, because some modules require params to be set up, which +# won't have happened earlier. + +# The only use for loading globals.pl is for Crypt(), which should at some +# point probably be factored out into Bugzilla::Auth::* + +# globals.pl clears the PATH, but File::Find uses Cwd::cwd() instead of +# Cwd::getcwd(), which we need to do because `pwd` isn't in the path - see +# http://www.xray.mpe.mpg.de/mailing-lists/perl5-porters/2001-09/msg00115.html +# As a workaround, since we only use File::Find in checksetup, which doesn't +# run in taint mode anyway, preserve the path... +my $origPath = $::ENV{'PATH'}; + +# Use the Bugzilla utility library for various functions. We do this +# here rather than at the top of the file so globals.pl doesn't define +# localconfig variables for us before we get a chance to check for +# their existence and create them if they don't exist. Also, globals.pl +# removes $ENV{'path'}, which we need in order to run `which mysql` above. +require "globals.pl"; + +# ...and restore it. This doesn't change tainting, so this will still cause +# errors if this script ever does run with -T. +$::ENV{'PATH'} = $origPath; ########################################################################### # Check MySQL setup @@ -1300,6 +1305,16 @@ my $dbh = DBI->connect($connectstring, $my_db_user, $my_db_pass) END { $dbh->disconnect if $dbh } +########################################################################### +# Check for LDAP +########################################################################### + +if (Param('loginmethod') eq 'LDAP') { + my $netLDAP = have_vers("Net::LDAP", 0); + if (!$netLDAP && !$silent) { + print "If you wish to use LDAP authentication, then you must install Net::LDAP\n\n"; + } +} ########################################################################### # Check GraphViz setup diff --git a/createaccount.cgi b/createaccount.cgi index 4ce347fcf..0550f42b9 100755 --- a/createaccount.cgi +++ b/createaccount.cgi @@ -40,11 +40,11 @@ use vars qw( ConnectToDatabase(); # If we're using LDAP for login, then we can't create a new account here. -if(Param('useLDAP')) { +unless (Bugzilla::Auth->can_edit) { # Just in case someone already has an account, let them get the correct # footer on the error message quietly_check_login(); - ThrowUserError("ldap_cant_create_account"); + ThrowUserError("auth_cant_create_account"); } # Clear out the login cookies. Make people log in again if they create an diff --git a/defparams.pl b/defparams.pl index f75ead4b2..31a7786ac 100644 --- a/defparams.pl +++ b/defparams.pl @@ -123,6 +123,31 @@ sub check_netmask { return ""; } +sub check_loginmethod { + # doeditparams traverses the list of params, and for each one it checks, + # then updates. This means that if one param checker wants to look at + # other params, it must be below that other one. So you can't have two + # params mutually dependant on each other. + # This means that if someone clears the LDAP config params after setting + # the login method as LDAP, we won't notice, but all logins will fail. + # So don't do that. + + my ($method, $entry) = @_; + my $res = check_multi($method, $entry); + return $res if $res; + if ($method eq 'DB') { + # No params + } elsif ($method eq 'LDAP') { + eval "require Net::LDAP"; + return "Error requiring Net::LDAP: '$@'" if $@; + return "LDAP servername is missing" unless Param("LDAPserver"); + return "LDAPBaseDN is empty" unless Param("LDAPBaseDN"); + } else { + return "Unknown loginmethod '$method' in check_loginmethod"; + } + return ""; +} + # OK, here are the parameter definitions themselves. # # Each definition is a hash with keys: @@ -323,16 +348,6 @@ sub check_netmask { }, { - name => 'useLDAP', - desc => 'Turn this on to use an LDAP directory for user authentication ' . - 'instead of the Bugzilla database. (User profiles will still be ' . - 'stored in the database, and will match against the LDAP user by ' . - 'email address.)', - type => 'b', - default => 0 - }, - - { name => 'LDAPserver', desc => 'The name (and optionally port) of your LDAP server. (e.g. ' . 'ldap.company.com, or ldap.company.com:portnum)', @@ -341,6 +356,16 @@ sub check_netmask { }, { + name => 'LDAPbinddn', + desc => 'If your LDAP server requires that you use a binddn and password ' . + 'instead of binding anonymously, enter it here ' . + '(e.g. cn=default,cn=user:password). ' . + 'Leave this empty for the normal case of an anonymous bind.', + type => 't', + default => '' + }, + + { name => 'LDAPBaseDN', desc => 'The BaseDN for authenticating users against. (e.g. ' . '"ou=People,o=Company")', @@ -349,6 +374,13 @@ sub check_netmask { }, { + name => 'LDAPuidattribute', + desc => 'The name of the attribute containing the user\'s login name.', + type => 't', + default => 'uid' + }, + + { name => 'LDAPmailattribute', desc => 'The name of the attribute of a user in your directory that ' . 'contains the email address.', @@ -357,6 +389,29 @@ sub check_netmask { }, { + name => 'loginmethod', + desc => 'The type of login authentication to use: + <dl> + <dt>DB</dt> + <dd> + Bugzilla\'s builtin authentication. This is the most common + choice. + </dd> + <dt>LDAP</dt> + <dd> + LDAP authentication using an LDAP server. This method is + experimental; please see the Bugzilla documentation for more + information. Using this method requires additional parameters + to be set above. + </dd> + </dl>', + type => 's', + choices => [ 'DB', 'LDAP' ], + default => 'DB', + checker => \&check_loginmethod + }, + + { name => 'mostfreqthreshold', desc => 'The minimum number of duplicates a bug needs to show up on the ' . '<a href="duplicates.cgi">most frequently reported bugs page</a>. ' . diff --git a/editusers.cgi b/editusers.cgi index cc6be6665..fee00a4e0 100755 --- a/editusers.cgi +++ b/editusers.cgi @@ -110,8 +110,8 @@ sub EmitFormElements ($$$$) if ($editall) { print "</TR><TR>\n"; print " <TH ALIGN=\"right\">Password:</TH>\n"; - if(Param('useLDAP')) { - print " <TD><FONT COLOR=RED>This site is using LDAP for authentication!</FONT></TD>\n"; + if(!Bugzilla::Auth->can_edit) { + print " <TD><FONT COLOR=RED>This site's authentication method does not allow password changes through Bugzilla!</FONT></TD>\n"; } else { print qq| <TD><INPUT TYPE="PASSWORD" SIZE="16" MAXLENGTH="16" NAME="password" VALUE=""><br> @@ -357,7 +357,7 @@ if ($action eq 'list') { } print "</TR>"; } - if ($editall && !Param('useLDAP')) { + if ($editall && Bugzilla::Auth->can_edit) { print "<TR>\n"; my $span = $candelete ? 3 : 2; print qq{ @@ -391,9 +391,8 @@ if ($action eq 'add') { exit; } - if(Param('useLDAP')) { - print "This site is using LDAP for authentication. To add a new user, "; - print "please contact the LDAP administrators."; + if(!Bugzilla::Auth->can_edit) { + print "The authentication mechanism you are using does not permit accounts to be created from Bugzilla"; PutTrailer(); exit; } @@ -429,9 +428,8 @@ if ($action eq 'new') { exit; } - if(Param('useLDAP')) { - print "This site is using LDAP for authentication. To add a new user, "; - print "please contact the LDAP administrators."; + if (!Bugzilla::Auth->can_edit) { + print "This site's authentication mechanism does not allow new users to be added."; PutTrailer(); exit; } @@ -791,7 +789,7 @@ if ($action eq 'update') { # Update the database with the user's new password if they changed it. - if ( !Param('useLDAP') && $editall && $password ) { + if ( Bugzilla::Auth->can_edit && $editall && $password ) { my $passworderror = ValidatePassword($password); if ( !$passworderror ) { my $cryptpassword = SqlQuote(Crypt($password)); diff --git a/t/Support/Files.pm b/t/Support/Files.pm index 108f6541a..e822346d0 100644 --- a/t/Support/Files.pm +++ b/t/Support/Files.pm @@ -29,12 +29,13 @@ package Support::Files; @additional_files = (); %exclude_deps = ( 'XML::Parser' => ['importxml.pl'], + 'Net::LDAP' => ['Bugzilla/Auth/LDAP.pm'], ); -# XXX - this file should be rewritten to use File::Find or similar +# XXX - this file should really be rewritten to use File::Find or similar $file = '*'; -@files = (glob($file), glob('Bugzilla/*.pm')); +@files = (glob($file), glob('Bugzilla/*.pm'), glob('Bugzilla/*/*.pm')); sub have_pkg { my ($pkg) = @_; diff --git a/template/en/default/account/auth/ldap-error.html.tmpl b/template/en/default/account/auth/ldap-error.html.tmpl new file mode 100644 index 000000000..7615fb326 --- /dev/null +++ b/template/en/default/account/auth/ldap-error.html.tmpl @@ -0,0 +1,48 @@ +[%# 1.0@bugzilla.org %] +[%# The contents of this file are subject to the Mozilla Public + # License Version 1.1 (the "License"); you may not use this file + # except in compliance with the License. You may obtain a copy of + # the License at http://www.mozilla.org/MPL/ + # + # Software distributed under the License is distributed on an "AS + # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + # implied. See the License for the specific language governing + # rights and limitations under the License. + # + # The Original Code is the Bugzilla Bug Tracking System. + # + # The Initial Developer of the Original Code is Netscape Communications + # Corporation. Portions created by Netscape are + # Copyright (C) 1998 Netscape Communications Corporation. All + # Rights Reserved. + # + # Contributor(s): Bradley Baetz <bbaetz@acm.org> + #%] + +[%# INTERFACE: + # auth_err_tag: string. The tag for the error + # info: hash. Additional variables which may be used when printing details + # of the error. + #%] + +[% SWITCH auth_err_tag %] + [% CASE "cannot_retreive_attr" %] + The specified LDAP attribute [% info.attr FILTER html %] was not found. + + [% CASE "connect_failed" %] + An error occurred while trying to connect to the LDAP server. + [% IF info.errstr %] + The error from the server was: <tt>[% info.errstr FILTER html %]</tt>. + [% END %] + + [% CASE "no_userid" %] + Bugzilla created a new account for you, but then could not find the + new userid. + + [% CASE "server_not_defined" %] + The LDAP server for authentication has not been defined. + + [% CASE %] + Unhandled authentication error: [% auth_err_tag FILTER html %] + +[% END %] diff --git a/template/en/default/account/login.html.tmpl b/template/en/default/account/auth/login.html.tmpl index 7d6e298d2..6dbd6531f 100644 --- a/template/en/default/account/login.html.tmpl +++ b/template/en/default/account/auth/login.html.tmpl @@ -31,45 +31,26 @@ %] <p> - I need a legitimate - [% Param('useLDAP') ? "LDAP username" : "email address" %] - and password to continue. -</p> + I need a legitimate login and password to continue. +</p> <form action="[% target %]" method="POST"> <table> <tr> - [% IF Param("useLDAP") %] - <td align="right"> - <b>Username:</b> - </td> - <td> - <input size="10" name="LDAP_login"> - </td> - </tr> - <tr> - <td align="right"> - <b>Password:</b> - </td> - <td> - <input type="password" size="10" name="LDAP_password"> - </td> - [% ELSE %] - <td align="right"> - <b>E-mail address:</b> - </td> - <td> - <input size="35" name="Bugzilla_login"> - </td> - </tr> - <tr> - <td align="right"> - <b>Password:</b> - </td> - <td> - <input type="password" size="35" name="Bugzilla_password"> - </td> - [% END %] + <td align="right"> + <b>Login:</b> + </td> + <td> + <input size="35" name="Bugzilla_login"> + </td> + </tr> + <tr> + <td align="right"> + <b>Password:</b> + </td> + <td> + <input type="password" size="35" name="Bugzilla_password"> + </td> [% IF Param('loginnetmask') < 32 %] <tr> <td align="right"> @@ -88,18 +69,17 @@ </tr> </table> - [% PROCESS "global/hidden-fields.html.tmpl" - exclude="^(Bugzilla|LDAP)_(login|password)$" %] + [% PROCESS "global/hidden-fields.html.tmpl" + exclude="^Bugzilla_(login|password|restrictlogin)$" %] <input type="submit" name="GoAheadAndLogIn" value="Login"> </form> [%# Allow the user to create a new account, or request a token to change - # their password (unless we are using LDAP, in which case the user must - # use LDAP to change it). + # their password, assuming that our auth method allows that. #%] -[% UNLESS Param("useLDAP") %] +[% IF caneditaccount %] <hr> [% IF Param("createemailregexp") %] diff --git a/template/en/default/admin/products/groupcontrol/confirm-edit.html.tmpl b/template/en/default/admin/products/groupcontrol/confirm-edit.html.tmpl index 85f89e6e8..cf8fcca9c 100644 --- a/template/en/default/admin/products/groupcontrol/confirm-edit.html.tmpl +++ b/template/en/default/admin/products/groupcontrol/confirm-edit.html.tmpl @@ -36,7 +36,7 @@ group '[% group.name FILTER html %]' impacts [% group.count %] bugs for which th [% END %] <form method="post" > - [% PROCESS "global/hidden-fields.html.tmpl" exclude="^(Bugzilla|LDAP)_(login|password)$" %] + [% PROCESS "global/hidden-fields.html.tmpl" exclude="^Bugzilla_(login|password)$" %] <br> Click "Continue" to proceed with the change including the changes diff --git a/template/en/default/bug/process/confirm-duplicate.html.tmpl b/template/en/default/bug/process/confirm-duplicate.html.tmpl index 22ae57b75..72472a83b 100644 --- a/template/en/default/bug/process/confirm-duplicate.html.tmpl +++ b/template/en/default/bug/process/confirm-duplicate.html.tmpl @@ -52,7 +52,7 @@ <form method="post" action="process_bug.cgi"> -[% PROCESS "global/hidden-fields.html.tmpl" exclude="^(Bugzilla|LDAP)_(login|password)$" %] +[% PROCESS "global/hidden-fields.html.tmpl" exclude="^Bugzilla_(login|password)$" %] <p> <input type="radio" name="confirm_add_duplicate" value="1"> diff --git a/template/en/default/bug/process/midair.html.tmpl b/template/en/default/bug/process/midair.html.tmpl index 85a5c1fff..e3698a640 100644 --- a/template/en/default/bug/process/midair.html.tmpl +++ b/template/en/default/bug/process/midair.html.tmpl @@ -65,7 +65,7 @@ You have the following choices: <ul> <li> <form method="post" action="process_bug.cgi"> - [% PROCESS "global/hidden-fields.html.tmpl" exclude="^(Bugzilla|LDAP)_(login|password)$" %] + [% PROCESS "global/hidden-fields.html.tmpl" exclude="^Bugzilla_(login|password)$" %] <input type="submit" value="Submit my changes anyway"> This will cause all of the above changes to be overwritten [% ", except for the added comment(s)" IF comments.size > start_at %]. diff --git a/template/en/default/global/code-error.html.tmpl b/template/en/default/global/code-error.html.tmpl index a29cb1e1c..b35bbb064 100644 --- a/template/en/default/global/code-error.html.tmpl +++ b/template/en/default/global/code-error.html.tmpl @@ -48,6 +48,16 @@ Attachment #[% attachid FILTER html %] ([% description FILTER html %]) is already obsolete. + [% ELSIF error == "auth_err" %] + [% title = "Internal Authentication Error" %] + [%# Authentication errors are in a template depending on the auth method, + for pluggability. + #%] + [% INCLUDE "account/auth/$authmethod-error.html.tmpl" %] + + [% ELSIF error == "authres_unhandled" %] + An authorization handler return value was not handled by the login code. + [% ELSIF error == "bug_error" %] Trying to retrieve bug [% bug.bug_id %] returned the error [% bug.error FILTER html %] diff --git a/template/en/default/global/confirm-user-match.html.tmpl b/template/en/default/global/confirm-user-match.html.tmpl index 92fa47a53..037f7385c 100644 --- a/template/en/default/global/confirm-user-match.html.tmpl +++ b/template/en/default/global/confirm-user-match.html.tmpl @@ -155,7 +155,7 @@ [% IF matchsuccess == 1 %] - [% PROCESS "global/hidden-fields.html.tmpl" exclude="^(Bugzilla|LDAP)_(login|password)$" %] + [% PROCESS "global/hidden-fields.html.tmpl" exclude="^Bugzilla_(login|password)$" %] <p> <input type="submit" value="Continue"> diff --git a/template/en/default/global/user-error.html.tmpl b/template/en/default/global/user-error.html.tmpl index 89f8cb7f3..11899fe70 100644 --- a/template/en/default/global/user-error.html.tmpl +++ b/template/en/default/global/user-error.html.tmpl @@ -92,6 +92,12 @@ Bug aliases cannot be longer than 20 characters. Please choose a shorter alias. + [% ELSIF error == "auth_cant_create_account" %] + [% title = "Can't create accounts" %] + This site is using an authentication scheme which does not permit + account creation. Please contact an administrator to get a new account + created. + [% ELSIF error == "authorization_failure" %] [% title = "Authorization Failed" %] You are not allowed to [% action %]. @@ -312,11 +318,6 @@ [% ELSIF error == "invalid_username_or_password" %] [% title = "Invalid Username Or Password" %] The username or password you entered is not valid. - - [% ELSIF error == "ldap_cant_create_account" %] - [% title = "Can't create LDAP accounts" %] - This site is using LDAP for authentication. Please contact - an LDAP administrator to get a new account created. [% ELSIF error == "login_needed_for_password_change" %] [% title = "Login Name Required" %] |