]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 300410: Bugzilla::Auth needs to be restructured to not require a BEGIN block
authormkanat%bugzilla.org <>
Fri, 12 May 2006 09:40:56 +0000 (09:40 +0000)
committermkanat%bugzilla.org <>
Fri, 12 May 2006 09:40:56 +0000 (09:40 +0000)
Patch By Max Kanat-Alexander <mkanat@bugzilla.org> r=LpSolit, a=myk

35 files changed:
Bugzilla.pm
Bugzilla/Auth.pm
Bugzilla/Auth/Login.pm [new file with mode: 0644]
Bugzilla/Auth/Login/CGI.pm [new file with mode: 0644]
Bugzilla/Auth/Login/Cookie.pm [new file with mode: 0644]
Bugzilla/Auth/Login/Env.pm [new file with mode: 0644]
Bugzilla/Auth/Login/Stack.pm [new file with mode: 0644]
Bugzilla/Auth/Login/WWW.pm [deleted file]
Bugzilla/Auth/Login/WWW/CGI.pm [deleted file]
Bugzilla/Auth/Login/WWW/CGI/Cookie.pm [deleted file]
Bugzilla/Auth/Login/WWW/Env.pm [deleted file]
Bugzilla/Auth/Persist/Cookie.pm [new file with mode: 0644]
Bugzilla/Auth/README [deleted file]
Bugzilla/Auth/Verify.pm [new file with mode: 0644]
Bugzilla/Auth/Verify/DB.pm
Bugzilla/Auth/Verify/LDAP.pm
Bugzilla/Auth/Verify/Stack.pm [new file with mode: 0644]
Bugzilla/Constants.pm
Bugzilla/User.pm
createaccount.cgi
query.cgi
relogin.cgi
show_bug.cgi
template/en/default/account/auth/ldap-error.html.tmpl [deleted file]
template/en/default/account/auth/login-small.html.tmpl
template/en/default/account/auth/login.html.tmpl
template/en/default/account/prefs/account.html.tmpl
template/en/default/admin/sudo.html.tmpl
template/en/default/global/code-error.html.tmpl
template/en/default/global/useful-links.html.tmpl
template/en/default/global/user-error.html.tmpl
template/en/default/index.html.tmpl
template/en/default/sidebar.xul.tmpl
token.cgi
userprefs.cgi

index eca0ebf63c70e4e017683c2d76a0abade20172fd..b3c6ba7ce7e88af8b79628ac4eb1c58efd715ca1 100644 (file)
@@ -26,7 +26,7 @@ package Bugzilla;
 use strict;
 
 use Bugzilla::Auth;
-use Bugzilla::Auth::Login::WWW;
+use Bugzilla::Auth::Persist::Cookie;
 use Bugzilla::CGI;
 use Bugzilla::Config;
 use Bugzilla::Constants;
@@ -160,7 +160,13 @@ sub sudo_request {
 
 sub login {
     my ($class, $type) = @_;
-    my $authenticated_user = Bugzilla::Auth::Login::WWW->login($type);
+
+    my $authorizer = new Bugzilla::Auth();
+    $type = LOGIN_REQUIRED if Bugzilla->cgi->param('GoAheadAndLogIn');
+    if (!defined $type || $type == LOGIN_NORMAL) {
+        $type = Param('requirelogin') ? LOGIN_REQUIRED : LOGIN_NORMAL;
+    }
+    my $authenticated_user = $authorizer->login($type);
     
     # At this point, we now know if a real person is logged in.
     # We must now check to see if an sudo session is in progress.
@@ -200,14 +206,15 @@ sub logout {
     return unless user->id;
 
     $option = LOGOUT_CURRENT unless defined $option;
-    Bugzilla::Auth::Login::WWW->logout($_user, $option);
+    Bugzilla::Auth::Persist::Cookie->logout({type => $option});
+    Bugzilla->logout_request() unless $option eq LOGOUT_KEEP_CURRENT;
 }
 
 sub logout_user {
     my ($class, $user) = @_;
     # When we're logging out another user we leave cookies alone, and
     # therefore avoid calling Bugzilla->logout() directly.
-    Bugzilla::Auth::Login::WWW->logout($user, LOGOUT_ALL);
+    Bugzilla::Auth::Persist::Cookie->logout({user => $user});
 }
 
 # just a compatibility front-end to logout_user that gets a user by id
index 4ea3d5bd6450d4715b032d89957cd43e10e7bfa4..d650658f57d0b170644c32775868a51014a1289d 100644 (file)
 #
 # Contributor(s): Bradley Baetz <bbaetz@acm.org>
 #                 Erik Stambaugh <erik@dasbistro.com>
+#                 Max Kanat-Alexander <mkanat@bugzilla.org>
 
 package Bugzilla::Auth;
 
 use strict;
+use fields qw(
+    _info_getter
+    _verifier
+    _persister
+);
 
-use Bugzilla::Config;
 use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Config;
+use Bugzilla::Auth::Login::Stack;
+use Bugzilla::Auth::Verify::Stack;
+use Bugzilla::Auth::Persist::Cookie;
+
+use Switch;
+
+sub new {
+    my ($class, $params) = @_;
+    my $self = fields::new($class);
+
+    $params            ||= {};
+    $params->{Login}   ||= Param('user_info_class') . ',Cookie';
+    $params->{Verify}  ||= Param('user_verify_class');
+
+    $self->{_info_getter} = new Bugzilla::Auth::Login::Stack($params->{Login});
+    $self->{_verifier} = new Bugzilla::Auth::Verify::Stack($params->{Verify});
+    # If we ever have any other login persistence methods besides cookies,
+    # this could become more configurable.
+    $self->{_persister} = new Bugzilla::Auth::Persist::Cookie();
+
+    return $self;
+}
 
-# The verification method that was successfully used upon login, if any
-my $current_verify_class = undef;
+sub login {
+    my ($self, $type) = @_;
+    my $dbh = Bugzilla->dbh;
 
-# 'inherit' from the main verify method
-BEGIN {
-    for my $verifyclass (split /,\s*/, Param("user_verify_class")) {
-        if ($verifyclass =~ /^([A-Za-z0-9_\.\-]+)$/) {
-            $verifyclass = $1;
-        } else {
-            die "Badly-named user_verify_class '$verifyclass'";
+    # Get login info from the cookie, form, environment variables, etc.
+    my $login_info = $self->{_info_getter}->get_login_info();
+
+    if ($login_info->{failure}) {
+        return $self->_handle_login_result($login_info, $type);
+    }
+
+    # Now verify his username and password against the DB, LDAP, etc.
+    if ($self->{_info_getter}->{successful}->requires_verification) {
+        $login_info = $self->{_verifier}->check_credentials($login_info);
+        if ($login_info->{failure}) {
+            return $self->_handle_login_result($login_info, $type);
         }
-        require "Bugzilla/Auth/Verify/" . $verifyclass . ".pm";
+        $login_info =
+          $self->{_verifier}->{successful}->create_or_update_user($login_info);
+    }
+    else {
+        $login_info = $self->{_verifier}->create_or_update_user($login_info);
+    }
+
+    if ($login_info->{failure}) {
+        return $self->_handle_login_result($login_info, $type);
+    }
+
+    # Make sure the user isn't disabled.
+    my $user = $login_info->{user};
+    if ($user->disabledtext) {
+        return $self->_handle_login_result({ failure => AUTH_DISABLED,
+                                              user    => $user }, $type);
     }
+    $user->set_authorizer($self);
+
+    return $self->_handle_login_result($login_info, $type);
+}
+
+sub can_change_password {
+    my ($self) = @_;
+    my $verifier = $self->{_verifier}->{successful};
+    $verifier  ||= $self->{_verifier};
+    my $getter   = $self->{_info_getter}->{successful};
+    $getter      = $self->{_info_getter} 
+        if (!$getter || $getter->isa('Bugzilla::Auth::Login::Cookie'));
+    return $verifier->can_change_password &&
+           $getter->user_can_create_account;
+}
+
+sub can_login {
+    my ($self) = @_;
+    return $self->{_info_getter}->can_login;
+}
+
+sub can_logout {
+    my ($self) = @_;
+    my $getter = $self->{_info_getter}->{successful};
+    # If there's no successful getter, we're not logged in, so of
+    # course we can't log out!
+    return 0 unless $getter;
+    return $getter->can_logout;
 }
 
-# PRIVATE
+sub user_can_create_account {
+    my ($self) = @_;
+    my $verifier = $self->{_verifier}->{successful};
+    $verifier  ||= $self->{_verifier};
+    my $getter   = $self->{_info_getter}->{successful};
+    $getter      = $self->{_info_getter}
+        if (!$getter || $getter->isa('Bugzilla::Auth::Login::Cookie'));
+    return $verifier->user_can_create_account
+           && $getter->user_can_create_account;
+}
 
-# A number of features, like password change requests, require the DB
-# verification method to be on the list.
-sub has_db {
-    for (split (/[\s,]+/, Param("user_verify_class"))) {
-        if (/^DB$/) {
-            return 1;
+sub can_change_email {
+    return $_[0]->user_can_create_account;
+}
+
+sub _handle_login_result {
+    my ($self, $result, $login_type) = @_;
+    my $dbh = Bugzilla->dbh;
+
+    my $user      = $result->{user};
+    my $fail_code = $result->{failure};
+
+    if (!$fail_code) {
+        if ($self->{_info_getter}->{successful}->requires_persistence) {
+            $self->{_persister}->persist_login($user);
         }
     }
-    return 0;
+    else {
+        switch ($fail_code) {
+            case AUTH_ERROR {
+                ThrowCodeError($result->{error}, $result->{details});
+            }
+            case AUTH_NODATA {
+                if ($login_type == LOGIN_REQUIRED) {
+                    # 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.
+                    $dbh->do("DELETE FROM logincookies WHERE " .
+                             $dbh->sql_to_days('NOW()') . " - " .
+                             $dbh->sql_to_days('lastused') . " > 30");
+                    $self->{_info_getter}->fail_nodata($self);
+                }
+                # Otherwise, we just return the "default" user.
+                $user = Bugzilla->user;
+            }
+
+            # 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)
+            case [AUTH_LOGINFAILED, AUTH_NO_SUCH_USER] {
+                ThrowUserError("invalid_username_or_password");
+            }
+
+            # The account may be disabled
+            case AUTH_DISABLED {
+                $self->{_persister}->logout();
+                # XXX This is NOT a good way to do this, architecturally.
+                $self->{_persister}->clear_browser_cookies();
+                # and throw a user error
+                ThrowUserError("account_disabled",
+                    {'disabled_reason' => $result->{user}->disabledtext});
+            }
+
+            # If we get here, then we've run out of options, which 
+            # shouldn't happen.
+            else {
+                ThrowCodeError("authres_unhandled", 
+                               { value => $fail_code });
+            }
+        }
+    }
+
+    return $user;
 }
 
 # Returns the network address for a given IP
@@ -72,254 +213,300 @@ sub get_netaddr {
     return "0.0.0.0" if ($maskbits == 0);
 
     $addr >>= (32-$maskbits);
+
     $addr <<= (32-$maskbits);
     return join(".", unpack("CCCC", pack("N", $addr)));
 }
 
-# This is a replacement for the inherited authenticate function
-# go through each of the available methods for each function
-sub authenticate {
-    my $class = shift;
-    my @args   = @_;
-    my @firstresult = ();
-    my @result = ();
-    my $current_verify_method;
-    for my $method (split /,\s*/, Param("user_verify_class")) {
-        $current_verify_method = $method;
-        $method = "Bugzilla::Auth::Verify::" . $method;
-        @result = $method->authenticate(@args);
-        @firstresult = @result unless @firstresult;
-
-        if (($result[0] != AUTH_NODATA)&&($result[0] != AUTH_LOGINFAILED)) {
-            unshift @result, ($current_verify_method);
-            return @result;
-        }
-    }
-    @result = @firstresult;
-    # no auth match
-
-    # see if we can set $current to the first verify method that
-    # will allow a new login
-
-    my $chosen_verify_method;
-    for my $method (split /,\s*/, Param("user_verify_class")) {
-        $current_verify_method = $method;
-        $method = "Bugzilla::Auth::Verify::" . $method;
-        if ($method->can_edit('new')) {
-            $chosen_verify_method = $method;
-        }
-    }
-
-    unshift @result, $chosen_verify_method;
-    return @result;
-}
-
-sub can_edit {
-    my ($class, $type) = @_;
-    if ($current_verify_class) {
-        return $current_verify_class->can_edit($type);
-    }
-    # $current_verify_class will not be set if the user isn't logged in.  That
-    # happens when the user is trying to create a new account, which (for now)
-    # is hard-coded to work with DB.
-    elsif (has_db) {
-        return Bugzilla::Auth::Verify::DB->can_edit($type);
-    }
-    return 0;
-}
-
 1;
-
 __END__
 
 =head1 NAME
 
-Bugzilla::Auth - Authentication handling for Bugzilla users
+Bugzilla::Auth - An object that authenticates the login credentials for
+                 a user.
 
 =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).
+used to obtain the username/password (from CGI, email, etc), and the 
+other set uses this data to authenticate against the datasource 
+(the Bugzilla DB, LDAP, PAM, etc.).
+
+Modules for obtaining the username/password are subclasses of 
+L<Bugzilla::Auth::Login>, and modules for authenticating are subclasses
+of L<Bugzilla::Auth::Verify>.
+
+=head1 AUTHENTICATION ERROR CODES
+
+Whenever a method in the C<Bugzilla::Auth> family fails in some way,
+it will return a hashref containing at least a single key called C<failure>.
+C<failure> will point to an integer error code, and depending on the error
+code the hashref may contain more data.
+
+The error codes are explained here below.
+
+=head2 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.
+
+=head2 C<AUTH_ERROR>
+
+An error occurred when trying to use the login mechanism.
+
+The hashref will also contain an C<error> element, which is the name
+of an error from C<template/en/default/global/code-error.html> --
+the same type of error that would be thrown by 
+L<Bugzilla::Error::ThrowCodeError>.
+
+The hashref *may* contain an element called C<details>, which is a hashref
+that should be passed to L<Bugzilla::Error::ThrowCodeError> as the 
+various fields to be used in the error message.
+
+=head2 C<AUTH_LOGINFAILED>
+
+An incorrect username or password was given.
+
+=head2 C<AUTH_NO_SUCH_USER>
+
+This is an optional more-specific version of C<AUTH_LOGINFAILED>.
+Modules should throw this error when they discover that the
+requested user account actually does not exist, according to them.
+
+That is, for example, L<Bugzilla::Auth::Verify::LDAP> would throw
+this if the user didn't exist in LDAP.
 
-Modules for obtaining the data are located under L<Bugzilla::Auth::Login>, and
-modules for authenticating are located in L<Bugzilla::Auth::Verify>.
+The difference between C<AUTH_NO_SUCH_USER> and C<AUTH_LOGINFAILED>
+should never be communicated to the user, for security reasons.
+
+=head2 C<AUTH_DISABLED>
+
+The user successfully logged in, but their account has been disabled.
+Usually this is throw only by C<Bugzilla::Auth::login>.
+
+=head1 LOGIN TYPES
+
+The C<login> function (below) can do different types of login, depending
+on what constant you pass into it:
+
+=head2 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.
+
+=head2 C<LOGIN_NORMAL>
+
+A login may or may not be required, depending on the setting of the
+I<requirelogin> parameter. This is the default if you don't specify a
+type.
+
+=head2 C<LOGIN_REQUIRED>
+
+A login is always required to access this data.
 
 =head1 METHODS
 
-C<Bugzilla::Auth> contains several helper methods to be used by
-authentication or login modules.
+These are methods that can be called on a C<Bugzilla::Auth> object 
+itself.
+
+=head2 Login
 
 =over 4
 
-=item C<Bugzilla::Auth::get_netaddr($ipaddr)>
+=item C<login($type)>
 
-Given an ip address, this returns the associated network address, using
-C<Param('loginnetmask')> as the netmask. This can be used to obtain data
-in order to restrict weak authentication methods (such as cookies) to
-only some addresses.
+Description: Logs a user in. For more details on how this works
+             internally, see the section entitled "STRUCTURE."
+Params:      $type - One of the Login Types from above.
+Returns:     An authenticated C<Bugzilla::User>. Or, if the type was
+             not C<LOGIN_REQUIRED>, then we return an
+             empty C<Bugzilla::User> if no login data was passed in.
 
 =back
 
-=head1 AUTHENTICATION
+=head2 Info Methods
 
-Authentication modules check a user's credentials (username, password,
-etc) to verify who the user is.  The methods that C<Bugzilla::Auth> uses for
-authentication are wrappers that check all configured modules (via the
-C<Param('user_info_class')> and C<Param('user_verify_class')>) in sequence.
-
-=head2 METHODS
+These are methods that give information about the Bugzilla::Auth object.
 
 =over 4
 
-=item C<authenticate($username, $pass)>
+=item C<can_change_password>
 
-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.
+Description: Tells you whether or not the current login system allows
+             changing passwords.
+Params:      None
+Returns:     C<true> if users and administrators should be allowed to
+             change passwords, C<false> otherwise.
 
-The first return value is the name of the class that generated the results 
-constined in the remaining return values.  The second 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.
+=item C<can_login>
 
-=item C<AUTH_OK>
+Description: Tells you whether or not the current login system allows
+             users to log in through the web interface.
+Params:      None
+Returns:     C<true> if users can log in through the web interface,
+             C<false> otherwise.
 
-Authentication succeeded. The third variable is the userid of the new
-user.
+=item C<can_logout>
 
-=item C<AUTH_NODATA>
+Description: Tells you whether or not the current login system allows
+             users to log themselves out.
+Params:      None
+Returns:     C<true> if users can log themselves out, C<false> otherwise.
+             If a user isn't logged in, we always return C<false>.
 
-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<user_can_create_account>
+
+Description: Tells you whether or not users are allowed to manually create
+             their own accounts, based on the current login system in use.
+             Note that this doesn't check the C<createemailregexp>
+             parameter--you have to do that by yourself in your code.
+Params:      None
+Returns:     C<true> if users are allowed to create new Bugzilla accounts,
+             C<false> otherwise.
+
+=item C<can_change_email>
 
-=item C<AUTH_ERROR>
+Description: Whether or not the current login system allows users to
+             change their own email address.
+Params:      None
+Returns:     C<true> if users can change their own email address,
+             C<false> otherwise.
 
-An error occurred when trying to use the login mechanism. The third return
-value may contain the Bugzilla userid, but will probably be C<undef>,
-signifiying that the userid is unknown. The fourth value is a tag describing
-the error used by the authentication error templates to print a description
-to the user. The optional fifth argument is a hashref of values used as part
-of the tag's error descriptions.
+=back
 
-This error template must have a name/location of
-I<account/auth/C<lc(authentication-type)>-error.html.tmpl>.
+=head1 CLASS FUNCTIONS
 
-=item C<AUTH_LOGINFAILED>
+C<Bugzilla::Auth> contains several helper methods to be used by
+authentication or login modules.
 
-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 third 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.
+=over 4
 
-The fourth 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 fifth argument, to be used by the tag to give more detailed 
-information.
+=item C<Bugzilla::Auth::get_netaddr($ipaddr)>
 
-=item C<AUTH_DISABLED>
+Given an ip address, this returns the associated network address, using
+C<Param('loginnetmask')> as the netmask. This can be used to obtain data
+in order to restrict weak authentication methods (such as cookies) to
+only some addresses.
 
-The user successfully logged in, but their account has been disabled.
-The third argument in the returned array is the userid, and the fourth
-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<current_verify_class>
+=head1 STRUCTURE
 
-This scalar gets populated with the full name (eg.,
-C<Bugzilla::Auth::Verify::DB>) of the verification method being used by the
-current user.  If no user is logged in, it will contain the name of the first
-method that allows new users, if any.  Otherwise, it carries an undefined
-value.
+This section is mostly interesting to developers who want to implement
+a new authentication type. It describes the general structure of the
+Bugzilla::Auth family, and how the C<login> function works.
 
-=item C<can_edit>
+A C<Bugzilla::Auth> object is essentially a collection of a few other
+objects: the "Info Getter," the "Verifier," and the "Persistence 
+Mechanism."
 
-This determines if the user's account details can be modified.  It returns a
-reference to a hash with the keys C<userid>, C<login_name>, and C<realname>,
-which determine whether their respective profile values may be altered, and
-C<new>, which determines if new accounts may be created.
+They are used inside the C<login> function in the following order:
 
-Each user verification method (chosen with C<Param('user_verify_class')> has
-its own set of can_edit values.  Calls to can_edit return the appropriate
-values for the current user's login method.
+=head2 The Info Getter
 
-If a user is not logged in, C<can_edit> will contain the values of the first
-verify method that allows new users to be created, if available.  Otherwise it
-returns an empty hash.
+This is a C<Bugzilla::Auth::Login> object. Basically, it gets the
+username and password from the user, somehow. Or, it just gets enough
+information to uniquely identify a user, and passes that on down the line.
+(For example, a C<user_id> is enough to uniquely identify a user,
+even without a username and password.)
 
-=back
+Some Info Getters don't require any verification. For example, if we got
+the C<user_id> from a Cookie, we don't need to check the username and 
+password.
 
-=head1 LOGINS
+If an Info Getter returns only a C<user_id> and no username/password,
+then it MUST NOT require verification. If an Info Getter requires
+verfication, then it MUST return at least a C<username>.
 
-A login module can be used to try to log in a Bugzilla user in a
-particular way. For example,
-L<Bugzilla::Auth::Login::WWW::CGI|Bugzilla::Auth::Login::WWW::CGI>
-logs in users from CGI scripts, first by using form variables, and then
-by trying cookies as a fallback.
+=head2 The Verifier
 
-The login interface consists of the following methods:
+This verifies that the username and password are valid.
 
-=over 4
+It's possible that some methods of verification don't require a password.
 
-=item C<login>, which takes a C<$type> argument, using constants found in
-C<Bugzilla::Constants>.
+=head2 The Persistence Mechanism
 
-The login method may use various authentication modules (described
-above) to try to authenticate a user, and should return the userid on
-success, or C<undef> on failure.
+This makes it so that the user doesn't have to log in on every page.
+Normally this object just sends a cookie to the user's web browser,
+as that's the most common method of "login persistence."
 
-When a login is required, but data is not present, it is the job of the
-login method to prompt the user for this data.
+=head2 Other Things We Do
 
-The constants accepted by C<login> include the following:
+After we verify the username and password, sometimes we automatically
+create an account in the Bugzilla database, for certain authentication
+types. We use the "Account Source" to get data about the user, and
+create them in the database. (Or, if their data has changed since the
+last time they logged in, their data gets updated.)
 
-=item C<LOGIN_OPTIONAL>
+=head2 The C<$login_data> Hash
 
-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.
+All of the C<Bugzilla::Auth::Login> and C<Bugzilla::Auth::Verify>
+methods take an argument called C<$login_data>. This is basically
+a hash that becomes more and more populated as we go through the
+C<login> function.
 
-=item C<LOGIN_NORMAL>
+All C<Bugzilla::Auth::Login> and C<Bugzilla::Auth::Verify> methods
+also *return* the C<$login_data> structure, when they succeed. They
+may have added new data to it.
 
-A login may or may not be required, depending on the setting of the
-I<requirelogin> parameter.
+For all C<Bugzilla::Auth::Login> and C<Bugzilla::Auth::Verify> methods,
+the rule is "you must return the same hashref you were passed in." You can
+modify the hashref all you want, but you can't create a new one. The only
+time you can return a new one is if you're returning some error code
+instead of the C<$login_data> structure.
 
-=item C<LOGIN_REQUIRED>
+Each C<Bugzilla::Auth::Login> or C<Bugzilla::Auth::Verify> method
+explains in its documentation which C<$login_data> elements are
+required by it, and which are set by it.
 
-A login is always required to access this data.
+Here are all of the elements that *may* be in C<$login_data>:
 
-=item C<logout>, which takes a C<Bugzilla::User> argument for the user
-being logged out, and an C<$option> argument. Possible values for
-C<$option> include:
+=over 4
 
-=item C<LOGOUT_CURRENT>
+=item C<user_id>
 
-Log out the user and invalidate his currently registered session.
+A Bugzilla C<user_id> that uniquely identifies a user.
 
-=item C<LOGOUT_ALL>
+=item C<username>
 
-Log out the user, and invalidate all sessions the user has registered in
-Bugzilla.
+The username that was provided by the user.
 
-=item C<LOGOUT_KEEP_CURRENT>
+=item C<bz_username>
 
-Invalidate all sessions the user has registered excluding his current
-session; this option should leave the user logged in. This is useful for
-user-performed password changes.
+The username of this user inside of Bugzilla. Sometimes this differs from
+C<username>.
 
-=back
+=item C<password>
+
+The password provided by the user.
+
+=item C<realname>
 
-=head1 SEE ALSO
+The real name of the user.
+
+=item C<extern_id>
+
+Some string that uniquely identifies the user in an external account 
+source. If this C<extern_id> already exists in the database with
+a different username, the username will be *changed* to be the
+username specified in this C<$login_data>.
+
+That is, let's my extern_id is C<mkanat>. I already have an account
+in Bugzilla with the username of C<mkanat@foo.com>. But this time,
+when I log in, I have an extern_id of C<mkanat> and a C<username>
+of C<mkanat@bar.org>. So now, Bugzilla will automatically change my 
+username to C<mkanat@bar.org> instead of C<mkanat@foo.com>.
+
+=item C<user>
+
+A L<Bugzilla::User> object representing the authenticated user. 
+Note that C<Bugzilla::Auth::login> may modify this object at various points.
+
+=back
 
-L<Bugzilla::Auth::Login::WWW::CGI>, L<Bugzilla::Auth::Login::WWW::CGI::Cookie>, L<Bugzilla::Auth::Verify::DB>
 
diff --git a/Bugzilla/Auth/Login.pm b/Bugzilla/Auth/Login.pm
new file mode 100644 (file)
index 0000000..4a4c5f2
--- /dev/null
@@ -0,0 +1,125 @@
+# -*- 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.
+#
+# Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org>
+
+package Bugzilla::Auth::Login;
+
+use strict;
+use fields qw();
+
+# Determines whether or not a user can logout. It's really a subroutine,
+# but we implement it here as a constant. Override it in subclasses if
+# that particular type of login method cannot log out.
+use constant can_logout => 1;
+use constant can_login  => 1;
+use constant requires_persistence  => 1;
+use constant requires_verification => 1;
+use constant user_can_create_account => 0;
+
+sub new {
+    my ($class) = @_;
+    my $self = fields::new($class);
+    return $self;
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::Auth::Login - Gets username/password data from the user.
+
+=head1 DESCRIPTION
+
+Bugzilla::Auth::Login is used to get information that uniquely identifies
+a user and allows us to authorize their Bugzilla access.
+
+It is mostly an abstract class, requiring subclasses to implement
+most methods.
+
+Note that callers outside of the C<Bugzilla::Auth> package should never
+create this object directly. Just create a C<Bugzilla::Auth> object
+and call C<login> on it.
+
+=head1 LOGIN METHODS
+
+These are methods that have to do with getting the actual login data
+from the user or handling a login somehow.
+
+These methods are abstract -- they MUST be implemented by a subclass.
+
+=over 4
+
+=item C<get_login_info()>
+
+Description: Gets a username/password from the user, or some other
+             information that uniquely identifies them.
+Params:      None
+Returns:     A C<$login_data> hashref. (See L<Bugzilla::Auth> for details.)
+             The hashref MUST contain: C<user_id> *or* C<username>
+             If this is a login method that requires verification,
+             the hashref MUST contain C<password>.
+             The hashref MAY contain C<realname> and C<extern_id>.
+
+=item C<fail_nodata()>
+
+Description: This function is called when Bugzilla doesn't get
+             a username/password and the login type is C<LOGIN_REQUIRED>
+             (See L<Bugzilla::Auth> for a description of C<LOGIN_REQUIRED>).
+             That is, this handles C<AUTH_NODATA> in that situation.
+
+             This function MUST stop CGI execution when it is complete.
+             That is, it must call C<exit> or C<ThrowUserError> or some
+             such thing.
+Params:      None
+Returns:     Never Returns.
+
+=back
+
+=head1 INFO METHODS
+
+These are methods that describe the capabilities of this 
+C<Bugzilla::Auth::Login> object. These are all no-parameter
+methods that return either C<true> or C<false>.
+
+=over 4
+
+=item C<can_logout>
+
+Whether or not users can log out if they logged in using this
+object. Defaults to C<true>.
+
+=item C<can_login>
+
+Whether or not users can log in through the web interface using
+this object. Defaults to C<true>.
+
+=item C<requires_persistence>
+
+Whether or not we should send the user a cookie if they logged in with
+this method. Defaults to C<true>.
+
+=item C<requires_verification>
+
+Whether or not we should check the username/password that we
+got from this login method. Defaults to C<true>.
+
+=item C<user_can_create_account>
+
+Whether or not users can create accounts, if this login method is
+currently being used by the system. Defaults to C<false>.
+
+=back
diff --git a/Bugzilla/Auth/Login/CGI.pm b/Bugzilla/Auth/Login/CGI.pm
new file mode 100644 (file)
index 0000000..14b64ee
--- /dev/null
@@ -0,0 +1,73 @@
+# -*- 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>
+#                 Erik Stambaugh <erik@dasbistro.com>
+#                 Max Kanat-Alexander <mkanat@bugzilla.org>
+
+package Bugzilla::Auth::Login::CGI;
+use strict;
+use base qw(Bugzilla::Auth::Login);
+use constant user_can_create_account => 1;
+
+use Bugzilla::Config;
+use Bugzilla::Constants;
+use Bugzilla::Util;
+use Bugzilla::User;
+
+sub get_login_info {
+    my ($self) = @_;
+    my $cgi = Bugzilla->cgi;
+
+    my $username = trim($cgi->param("Bugzilla_login"));
+    my $password = $cgi->param("Bugzilla_password");
+
+    $cgi->delete('Bugzilla_login', 'Bugzilla_password');
+
+    if (!defined $username || !defined $password) {
+        return { failure => AUTH_NODATA };
+    }
+
+    return { username => $username, password => $password };
+}
+
+sub fail_nodata {
+    my ($self) = @_;
+    my $cgi = Bugzilla->cgi;
+    my $template = Bugzilla->template;
+
+    # Redirect to SSL if required
+    if (Param('sslbase') ne '' and Param('ssl') ne 'never') {
+        $cgi->require_https(Param('sslbase'));
+    }
+    print $cgi->header();
+    $template->process("account/auth/login.html.tmpl",
+                       { 'target' => $cgi->url(-relative=>1) }) 
+        || ThrowTemplateError($template->error());
+    exit;
+}
+
+1;
diff --git a/Bugzilla/Auth/Login/Cookie.pm b/Bugzilla/Auth/Login/Cookie.pm
new file mode 100644 (file)
index 0000000..e4cc0da
--- /dev/null
@@ -0,0 +1,83 @@
+# -*- 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.
+#
+# Contributor(s): Bradley Baetz <bbaetz@acm.org>
+#                 Max Kanat-Alexander <mkanat@bugzilla.org>
+
+package Bugzilla::Auth::Login::Cookie;
+use strict;
+use base qw(Bugzilla::Auth::Login);
+
+use Bugzilla::Auth;
+use Bugzilla::Constants;
+use Bugzilla::User;
+use Bugzilla::Util;
+
+use constant requires_persistence  => 0;
+use constant requires_verification => 0;
+use constant can_login => 0;
+
+# Note that Cookie never consults the Verifier, it always assumes
+# it has a valid DB account or it fails.
+sub get_login_info {
+    my ($self) = @_;
+    my $cgi = Bugzilla->cgi;
+    my $dbh = Bugzilla->dbh;
+
+    my $ip_addr      = $cgi->remote_addr();
+    my $net_addr     = Bugzilla::Auth::get_netaddr($ip_addr);
+    my $login_cookie = $cgi->cookie("Bugzilla_logincookie");
+    my $user_id      = $cgi->cookie("Bugzilla_login");
+
+    if ($login_cookie && $user_id) {
+        # Anything goes for these params - they're just strings which
+        # we're going to verify against the db
+        trick_taint($ip_addr);
+        trick_taint($login_cookie);
+        detaint_natural($user_id);
+
+        my $query = "SELECT userid
+                       FROM logincookies
+                      WHERE logincookies.cookie = ?
+                            AND logincookies.userid = ?
+                            AND (logincookies.ipaddr = ?";
+
+        # If we have a network block that's allowed to use this cookie,
+        # as opposed to just a single IP.
+        my @params = ($login_cookie, $user_id, $ip_addr);
+        if (defined $net_addr) {
+            trick_taint($net_addr);
+            $query .= " OR logincookies.ipaddr = ?";
+            push(@params, $net_addr);
+        }
+        $query .= ")";
+
+        # If the cookie is valid, return a valid username.
+        if ($dbh->selectrow_array($query, undef, @params)) {
+            # If we logged in successfully, then update the lastused 
+            # time on the login cookie
+            $dbh->do("UPDATE logincookies SET lastused = NOW() 
+                       WHERE cookie = ?", undef, $login_cookie);
+            return { user_id => $user_id };
+        }
+    }
+
+    # Either the he cookie is invalid, or we got no cookie. We don't want 
+    # to ever return AUTH_LOGINFAILED, because we don't want Bugzilla to 
+    # actually throw an error when it gets a bad cookie. It should just 
+    # look like there was no cokie to begin with.
+    return { failure => AUTH_NODATA };
+}
+
+1;
diff --git a/Bugzilla/Auth/Login/Env.pm b/Bugzilla/Auth/Login/Env.pm
new file mode 100644 (file)
index 0000000..fda71bf
--- /dev/null
@@ -0,0 +1,54 @@
+# -*- 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): Erik Stambaugh <erik@dasbistro.com>
+#                 Max Kanat-Alexander <mkanat@bugzilla.org>
+
+package Bugzilla::Auth::Login::Env;
+use strict;
+use base qw(Bugzilla::Auth::Login);
+
+use Bugzilla::Config;
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::User;
+
+use constant can_logout => 0;
+use constant can_login  => 0;
+use constant requires_verification => 0;
+
+sub get_login_info {
+    my ($self) = @_;
+    my $dbh = Bugzilla->dbh;
+
+    my $env_id       = $ENV{Param("auth_env_id")} || '';
+    my $env_email    = $ENV{Param("auth_env_email")} || '';
+    my $env_realname = $ENV{Param("auth_env_realname")} || '';
+
+    return { failure => AUTH_NODATA } if !$env_email;
+
+    return { username => $env_email, extern_id => $env_id, 
+             realname => $env_realname };
+}
+
+sub fail_nodata {
+    ThrowCodeError('env_no_email');
+}
+
+1;
diff --git a/Bugzilla/Auth/Login/Stack.pm b/Bugzilla/Auth/Login/Stack.pm
new file mode 100644 (file)
index 0000000..d510038
--- /dev/null
@@ -0,0 +1,87 @@
+# -*- 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): Max Kanat-Alexander <mkanat@bugzilla.org>
+
+package Bugzilla::Auth::Login::Stack;
+use strict;
+use base qw(Bugzilla::Auth::Login);
+use fields qw(
+    _stack
+    successful
+);
+
+sub new {
+    my $class = shift;
+    my $self = $class->SUPER::new(@_);
+    my $list = shift;
+    $self->{_stack} = [];
+    foreach my $login_method (split(',', $list)) {
+        require "Bugzilla/Auth/Login/${login_method}.pm";
+        push(@{$self->{_stack}}, 
+             "Bugzilla::Auth::Login::$login_method"->new(@_));
+    }
+    return $self;
+}
+
+sub get_login_info {
+    my $self = shift;
+    my $result;
+    foreach my $object (@{$self->{_stack}}) {
+        $result = $object->get_login_info(@_);
+        $self->{successful} = $object;
+        last if !$result->{failure};
+        # So that if none of them succeed, it's undef.
+        $self->{successful} = undef;
+    }
+    return $result;
+}
+
+sub fail_nodata {
+    my $self = shift;
+    # We fail from the bottom of the stack.
+    my @reverse_stack = reverse @{$self->{_stack}};
+    foreach my $object (@reverse_stack) {
+        # We pick the first object that actually has the method
+        # implemented.
+        if ($object->can('fail_nodata')) {
+            $object->fail_nodata(@_);
+        }
+    }
+}
+
+sub can_login {
+    my ($self) = @_;
+    # We return true if any method can log in.
+    foreach my $object (@{$self->{_stack}}) {
+        return 1 if $object->can_login;
+    }
+    return 0;
+}
+
+sub user_can_create_account {
+    my ($self) = @_;
+    # We return true if any method allows users to create accounts.
+    foreach my $object (@{$self->{_stack}}) {
+        return 1 if $object->user_can_create_account;
+    }
+    return 0;
+}
+
+1;
diff --git a/Bugzilla/Auth/Login/WWW.pm b/Bugzilla/Auth/Login/WWW.pm
deleted file mode 100644 (file)
index 29cc7fc..0000000
+++ /dev/null
@@ -1,111 +0,0 @@
-# -*- 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): Erik Stambaugh <erik@dasbistro.com>
-
-package Bugzilla::Auth::Login::WWW;
-
-use strict;
-
-use Bugzilla::Constants;
-use Bugzilla::Config;
-
-# $current_login_class stores the name of the login style that succeeded.
-my $current_login_class = undef;
-sub login_class {
-    my ($class, $type) = @_;
-    if ($type) {
-        $current_login_class = $type;
-    }
-    return $current_login_class;
-}
-
-# can_logout determines if a user may log out
-sub can_logout {
-    return 1 if (login_class && login_class->can_logout);
-    return 0;
-}
-
-sub login {
-    my ($class, $type) = @_;
-
-    my $user = Bugzilla->user;
-
-    # Avoid double-logins, which may confuse the auth code
-    # (double cookies, odd compat code settings, etc)
-    return $user if $user->id;
-
-    $type = LOGIN_REQUIRED if Bugzilla->cgi->param('GoAheadAndLogIn');
-    $type = LOGIN_NORMAL unless defined $type;
-
-    # Log in using whatever methods are defined in user_info_class.
-    # Please note the particularly strange way require() and the function
-    # calls are being done, because we're calling a module that's named in
-    # a string. I assure you it works, and it avoids the need for an eval().
-    my $userid;
-    for my $login_class (split(/,\s*/, Param('user_info_class'))) {
-        require "Bugzilla/Auth/Login/WWW/" . $login_class . ".pm";
-        $userid = "Bugzilla::Auth::Login::WWW::$login_class"->login($type);
-        if ($userid) {
-            $class->login_class("Bugzilla::Auth::Login::WWW::$login_class");
-            last;
-        }
-    }
-
-    if ($userid) {
-        $user = new Bugzilla::User($userid);
-
-        # Redirect to SSL if required
-        if (Param('sslbase') ne '' and Param('ssl') ne 'never') {
-            Bugzilla->cgi->require_https(Param('sslbase'));
-        }
-        $user->set_flags('can_logout' => $class->can_logout);
-    } else {
-        Bugzilla->logout_request();
-    }
-    return $user;
-}
-
-sub logout {
-    my ($class, $user, $option) = @_;
-    if (can_logout) {
-        $class->login_class->logout($user, $option);
-    }
-}
-
-1;
-
-
-__END__
-
-=head1 NAME
-
-Bugzilla::Auth::Login::WWW - WWW login information gathering module
-
-=head1 METHODS
-
-=over
-
-=item C<login>
-
-Passes C<login> calls to each class defined in the param C<user_info_class>
-and returns a C<Bugzilla::User> object from the first one that successfully
-gathers user login information.
-
-=back
diff --git a/Bugzilla/Auth/Login/WWW/CGI.pm b/Bugzilla/Auth/Login/WWW/CGI.pm
deleted file mode 100644 (file)
index 5030691..0000000
+++ /dev/null
@@ -1,275 +0,0 @@
-# -*- 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>
-#                 Erik Stambaugh <erik@dasbistro.com>
-
-package Bugzilla::Auth::Login::WWW::CGI;
-
-use strict;
-
-use Bugzilla::Config;
-use Bugzilla::Constants;
-use Bugzilla::Error;
-use Bugzilla::Util;
-use Bugzilla::Token;
-
-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;
-    my $dbh = Bugzilla->dbh;
-
-    # First, try the actual login method against form variables
-    my $username = trim($cgi->param("Bugzilla_login"));
-    my $passwd = $cgi->param("Bugzilla_password");
-    
-    $cgi->delete('Bugzilla_login', 'Bugzilla_password');
-
-    # Perform the actual authentication, get the method name from the class name
-    my ($authmethod, $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 = Bugzilla::Auth::get_netaddr($ipaddr);
-        }
-
-        # The IP address is valid, at least for comparing with itself in a
-        # subsequent login
-        trick_taint($ipaddr);
-
-        my $logincookie = Bugzilla::Token::GenerateUniqueToken('logincookies', 'cookie');
-
-        $dbh->do("INSERT INTO logincookies (cookie, userid, ipaddr, lastused)
-                 VALUES (?, ?, ?, NOW())",
-                 undef,
-                 $logincookie, $userid, $ipaddr);
-
-        # Remember cookie only if admin has told so
-        # or admin didn't forbid it and user told to remember.
-        if ((Param('rememberlogin') eq 'on') ||
-            ((Param('rememberlogin') ne 'off') &&
-             $cgi->param('Bugzilla_remember') &&
-             ($cgi->param('Bugzilla_remember') eq 'on'))) {
-            $cgi->send_cookie(-name => 'Bugzilla_login',
-                              -value => $userid,
-                              -expires => 'Fri, 01-Jan-2038 00:00:00 GMT');
-            $cgi->send_cookie(-name => 'Bugzilla_logincookie',
-                              -value => $logincookie,
-                              -expires => 'Fri, 01-Jan-2038 00:00:00 GMT');
-
-        }
-        else {
-            $cgi->send_cookie(-name => 'Bugzilla_login',
-                              -value => $userid);
-            $cgi->send_cookie(-name => 'Bugzilla_logincookie',
-                              -value => $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::Login::WWW::CGI::Cookie;
-        my $authmethod = "Cookie";
-
-        ($authres, $userid, $extra) =
-          Bugzilla::Auth::Login::WWW::CGI::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) {
-        ThrowCodeError("auth_err",
-                       { authmethod => lc($authmethod),
-                         userid => $userid,
-                         auth_err_tag => $extra,
-                         info => $info
-                       });
-    }
-
-    # 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) {
-
-        # Redirect to SSL if required
-        if (Param('sslbase') ne '' and Param('ssl') ne 'never') {
-            $cgi->require_https(Param('sslbase'));
-        }
-    
-        # Throw up the login page
-
-        print Bugzilla->cgi->header();
-
-        my $template = Bugzilla->template;
-        $template->process("account/auth/login.html.tmpl",
-                           { 'target' => $cgi->url(-relative=>1),
-                             'caneditaccount' => Bugzilla::Auth->can_edit('new'),
-                             'has_db' => Bugzilla::Auth->has_db,
-                           }
-                          )
-          || 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.
-        $dbh->do("DELETE FROM logincookies WHERE " .
-                 $dbh->sql_to_days('NOW()') . " - " .
-                 $dbh->sql_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_browser_cookies();
-        # 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 });
-}
-
-# This auth style allows the user to log out.
-sub can_logout { return 1; }
-
-# Logs user out, according to the option provided; this consists of
-# removing entries from logincookies for the specified $user.
-sub logout {
-    my ($class, $user, $option) = @_;
-    my $dbh = Bugzilla->dbh;
-    my $cgi = Bugzilla->cgi;
-    $option = LOGOUT_ALL unless defined $option;
-
-    if ($option == LOGOUT_ALL) {
-            $dbh->do("DELETE FROM logincookies WHERE userid = ?",
-                     undef, $user->id);
-            return;
-    }
-
-    # The LOGOUT_*_CURRENT options require the current login cookie.
-    # If a new cookie has been issued during this run, that's the current one.
-    # If not, it's the one we've received.
-    my $cookie;
-    foreach (@{$cgi->{'Bugzilla_cookie_list'}}) {
-        if ($_->name() eq 'Bugzilla_logincookie') {
-            $cookie = $_->value();
-            last;
-        }
-    }
-    $cookie ||= $cgi->cookie("Bugzilla_logincookie");
-    trick_taint($cookie);
-
-    # These queries use both the cookie ID and the user ID as keys. Even
-    # though we know the userid must match, we still check it in the SQL
-    # as a sanity check, since there is no locking here, and if the user
-    # logged out from two machines simultaneously, while someone else
-    # logged in and got the same cookie, we could be logging the other
-    # user out here. Yes, this is very very very unlikely, but why take
-    # chances? - bbaetz
-    if ($option == LOGOUT_KEEP_CURRENT) {
-        $dbh->do("DELETE FROM logincookies WHERE cookie != ? AND userid = ?",
-                 undef, $cookie, $user->id);
-    } elsif ($option == LOGOUT_CURRENT) {
-        $dbh->do("DELETE FROM logincookies WHERE cookie = ? AND userid = ?",
-                 undef, $cookie, $user->id);
-    } else {
-        die("Invalid option $option supplied to logout()");
-    }
-
-    if ($option != LOGOUT_KEEP_CURRENT) {
-        clear_browser_cookies();
-        Bugzilla->logout_request();
-    }
-}
-
-sub clear_browser_cookies {
-    my $cgi = Bugzilla->cgi;
-    $cgi->remove_cookie('Bugzilla_login');
-    $cgi->remove_cookie('Bugzilla_logincookie');
-}
-
-1;
-
-__END__
-
-=head1 NAME
-
-Bugzilla::Auth::Login::WWW::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. Logouts are also handled here.
-
-=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::Login::WWW::CGI::Cookie>.
-
-=head1 SEE ALSO
-
-L<Bugzilla::Auth>
diff --git a/Bugzilla/Auth/Login/WWW/CGI/Cookie.pm b/Bugzilla/Auth/Login/WWW/CGI/Cookie.pm
deleted file mode 100644 (file)
index c2244d1..0000000
+++ /dev/null
@@ -1,113 +0,0 @@
-# -*- 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::Login::WWW::CGI::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=?";
-    my @params = ($login_cookie, $login, $ipaddr);
-    if (defined $netaddr) {
-        trick_taint($netaddr);
-        $query .= " OR logincookies.ipaddr=?";
-        push(@params, $netaddr);
-    }
-    $query .= ")";
-
-    my $dbh = Bugzilla->dbh;
-    my ($userid, $disabledtext) = $dbh->selectrow_array($query, undef, @params);
-
-    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=NOW() WHERE cookie=?",
-                 undef,
-                 $login_cookie);
-
-        return (AUTH_OK, $userid);
-    }
-
-    # If we get here, then the login failed.
-    return (AUTH_LOGINFAILED);
-}
-
-1;
-
-__END__
-
-=head1 NAME
-
-Bugzilla::Auth::Login::WWW::CGI::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::Login::WWW::CGI> handles this.
-
-=head1 SEE ALSO
-
-L<Bugzilla::Auth>, L<Bugzilla::Auth::Login::WWW::CGI>
diff --git a/Bugzilla/Auth/Login/WWW/Env.pm b/Bugzilla/Auth/Login/WWW/Env.pm
deleted file mode 100644 (file)
index f437bf0..0000000
+++ /dev/null
@@ -1,156 +0,0 @@
-# -*- 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): Erik Stambaugh <erik@dasbistro.com>
-
-package Bugzilla::Auth::Login::WWW::Env;
-
-use strict;
-
-use Bugzilla::Config;
-use Bugzilla::Error;
-use Bugzilla::Util;
-use Bugzilla::User;
-
-sub login {
-    my ($class, $type) = @_;
-    my $dbh = Bugzilla->dbh;
-
-    # XXX This does not currently work correctly with Param('requirelogin').
-    #     Bug 253636 will hopefully see that param's needs taken care of in a
-    #     parent module, but for the time being, this module does not honor
-    #     the param in the way that CGI.pm does.
-
-    my $matched_userid;
-    my $matched_extern_id;
-    my $disabledtext;
-
-    # Gather the environment variables
-    my $env_id       = $ENV{Param("auth_env_id")} || '';
-    my $env_email    = $ENV{Param("auth_env_email")} || '';
-    my $env_realname = $ENV{Param("auth_env_realname")} || '';
-
-    # make sure the email field contains only a valid email address
-    my $emailregexp = Param("emailregexp");
-    if ($env_email =~ /($emailregexp)/) {
-        $env_email = $1;
-    }
-    else {
-        $env_email = '';
-    }
-
-    return undef unless $env_email;
-
-    # untaint the remaining values
-    trick_taint($env_id);
-    trick_taint($env_realname);
-
-    # Look in the DB for the extern_id
-    if ($env_id) {
-        ($matched_userid, $disabledtext) =
-            $dbh->selectrow_array('SELECT userid, disabledtext
-                                   FROM profiles WHERE extern_id = ?',
-                                   undef, $env_id);
-    }
-
-    unless ($matched_userid) {
-        # There was either no match for the external ID given, or one was
-        # not present.
-        #
-        # Check to see if the email address is in there and has no
-        # external id assigned.  We test for both the login name (which we
-        # also sent), and the id, so that we have a way of telling that we
-        # got something instead of a bunch of NULLs
-        ($matched_extern_id, $matched_userid, $disabledtext) =
-            $dbh->selectrow_array('SELECT extern_id, userid, disabledtext
-                                   FROM profiles WHERE ' .
-                                   $dbh->sql_istrcmp('login_name', '?'),
-                                   undef, $env_email);
-
-        if ($matched_userid) {
-            if ($matched_extern_id) {
-                # someone with a different external ID has that address!
-                ThrowUserError("extern_id_conflict");
-            }
-            else {
-                # someone with no external ID used that address, time to
-                # add the ID!
-                $dbh->do('UPDATE profiles SET extern_id = ? WHERE userid = ?',
-                         undef,($env_id, $matched_userid));
-            }
-        }
-        else {
-            # Need to create a new user with that email address.  Note
-            # that cryptpassword has been filled in with '*', since the
-            # user has no DB password.
-            insert_new_user($env_email, $env_realname, '*');
-            my $new_user = Bugzilla::User->new_from_login($env_email);
-            $matched_userid = $new_user->id;
-        }
-    }
-
-    # now that we hopefully have a username, we need to see if the data
-    # has to be updated. If we just created this account, then the data
-    # is already up to date.
-    my ($username, $this_realname) =
-        $dbh->selectrow_array('SELECT login_name, realname
-                               FROM profiles WHERE userid = ?',
-                               undef, $matched_userid);
-
-    if (($username ne $env_email) || ($this_realname ne $env_realname)) {
-        $dbh->do('UPDATE profiles SET login_name = ?, realname = ?
-                  WHERE userid = ?', undef,
-                  ($env_email, ($env_realname || $this_realname), $matched_userid));
-
-        # If the login name may be new, make sure the regexp groups are current
-        my $userprofile = new Bugzilla::User($matched_userid);
-        $userprofile->derive_regexp_groups;
-    }
-
-    # Now we throw an error if the user has been disabled
-    if ($disabledtext) {
-        ThrowUserError("account_disabled",
-                       {'disabled_reason' => $disabledtext});
-    }
-
-    return $matched_userid;
-}
-
-# This auth style does not allow the user to log out.
-sub can_logout { return 0; }
-
-1;
-
-__END__
-
-=head1 NAME
-
-Bugzilla::Auth::Env - Environment Variable Authentication
-
-=head1 DESCRIPTION
-
-Many external user authentication systems supply login information to CGI
-programs via environment variables.  This module checks to see if those
-variables are populated and, if so, assumes authentication was successful and
-returns the user's ID, having automatically created a new profile if
-necessary.
-
-=head1 SEE ALSO
-
-L<Bugzilla::Auth>
diff --git a/Bugzilla/Auth/Persist/Cookie.pm b/Bugzilla/Auth/Persist/Cookie.pm
new file mode 100644 (file)
index 0000000..ce59ef4
--- /dev/null
@@ -0,0 +1,153 @@
+# -*- 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>
+#                 Erik Stambaugh <erik@dasbistro.com>
+#                 Max Kanat-Alexander <mkanat@bugzilla.org>
+
+package Bugzilla::Auth::Persist::Cookie;
+use strict;
+use fields qw();
+
+use Bugzilla::Config;
+use Bugzilla::Constants;
+use Bugzilla::Util;
+use Bugzilla::User;
+
+use List::Util qw(first);
+
+sub new {
+    my ($class) = @_;
+    my $self = fields::new($class);
+    return $self;
+}
+
+sub persist_login {
+    my ($self, $user) = @_;
+    my $dbh = Bugzilla->dbh;
+    my $cgi = Bugzilla->cgi;
+
+    my $ip_addr = $cgi->remote_addr;
+    unless ($cgi->param('Bugzilla_restrictlogin') ||
+            Param('loginnetmask') == 32) 
+    {
+        # XXX I don't like this subclass being dependent upon its parent.
+        $ip_addr = Bugzilla::Auth::get_netaddr($ip_addr);
+    }
+
+    # The IP address is valid, at least for comparing with itself in a
+    # subsequent login
+    trick_taint($ip_addr);
+
+    my $login_cookie = 
+        Bugzilla::Token::GenerateUniqueToken('logincookies', 'cookie');
+
+    $dbh->do("INSERT INTO logincookies (cookie, userid, ipaddr, lastused)
+              VALUES (?, ?, ?, NOW())",
+              undef, $login_cookie, $user->id, $ip_addr);
+
+    # Remember cookie only if admin has told so
+    # or admin didn't forbid it and user told to remember.
+    if ( Param('rememberlogin') eq 'on' ||
+         (Param('rememberlogin') ne 'off' &&
+          $cgi->param('Bugzilla_remember') &&
+          $cgi->param('Bugzilla_remember') eq 'on') ) 
+    {
+        $cgi->send_cookie(-name => 'Bugzilla_login',
+                          -value => $user->id,
+                          -expires => 'Fri, 01-Jan-2038 00:00:00 GMT');
+        $cgi->send_cookie(-name => 'Bugzilla_logincookie',
+                          -value => $login_cookie,
+                          -expires => 'Fri, 01-Jan-2038 00:00:00 GMT');
+
+    }
+    else {
+        $cgi->send_cookie(-name => 'Bugzilla_login',
+                          -value => $user->id);
+        $cgi->send_cookie(-name => 'Bugzilla_logincookie',
+                          -value => $login_cookie);
+    }
+}
+
+sub logout {
+    my ($self, $param) = @_;
+
+    my $dbh = Bugzilla->dbh;
+    my $cgi = Bugzilla->cgi;
+    $param = {} unless $param;
+    my $user = $param->{user} || Bugzilla->user;
+    my $type = $param->{type} || LOGOUT_ALL;
+
+    if ($type == LOGOUT_ALL) {
+        $dbh->do("DELETE FROM logincookies WHERE userid = ?",
+                 undef, $user->id);
+        return;
+    }
+
+    # The LOGOUT_*_CURRENT options require the current login cookie.
+    # If a new cookie has been issued during this run, that's the current one.
+    # If not, it's the one we've received.
+    my $cookie = first {$_->name eq 'Bugzilla_logincookie'}
+                       @{$cgi->{'Bugzilla_cookie_list'}};
+    my $login_cookie;
+    if ($cookie) {
+        $login_cookie = $cookie->value;
+    }
+    else {
+        $login_cookie = $cgi->cookie("Bugzilla_logincookie");
+    }
+    trick_taint($login_cookie);
+
+    # These queries use both the cookie ID and the user ID as keys. Even
+    # though we know the userid must match, we still check it in the SQL
+    # as a sanity check, since there is no locking here, and if the user
+    # logged out from two machines simultaneously, while someone else
+    # logged in and got the same cookie, we could be logging the other
+    # user out here. Yes, this is very very very unlikely, but why take
+    # chances? - bbaetz
+    if ($type == LOGOUT_KEEP_CURRENT) {
+        $dbh->do("DELETE FROM logincookies WHERE cookie != ? AND userid = ?",
+                 undef, $login_cookie, $user->id);
+    } elsif ($type == LOGOUT_CURRENT) {
+        $dbh->do("DELETE FROM logincookies WHERE cookie = ? AND userid = ?",
+                 undef, $login_cookie, $user->id);
+    } else {
+        die("Invalid type $type supplied to logout()");
+    }
+
+    if ($type != LOGOUT_KEEP_CURRENT) {
+        clear_browser_cookies();
+    }
+
+}
+
+sub clear_browser_cookies {
+    my $cgi = Bugzilla->cgi;
+    $cgi->remove_cookie('Bugzilla_login');
+    $cgi->remove_cookie('Bugzilla_logincookie');
+}
+
+1;
diff --git a/Bugzilla/Auth/README b/Bugzilla/Auth/README
deleted file mode 100644 (file)
index e573e2c..0000000
+++ /dev/null
@@ -1,132 +0,0 @@
-How Auth Works
-==============
-Christian Reis <kiko@async.com.br>
-
-Overview
---------
-
-Authentication in Bugzilla is handled by a collection of modules that live in
-the Bugzilla::Auth package.  These modules are organized hierarchically based
-upon their responsibility.
-
-The authentication scheme is divided in two tasks: Login and Verify.  Login
-involves gathering credentials from a user, while Verify validates them
-against an authentication service.
-
-The Bugzilla parameters user_info_class and user_verify_class contain a
-list of Login and Verify modules, respectively.
-
-Task: Login
------------
-
-This task obtains user credentials based on a request. Examples of requests
-include CGI access from the Bugzilla web interface, email submissions and
-credentials supplied by standalone scripts.
-
-Each type of Bugzilla front-end should have its own package.  For instance,
-access via the Bugzilla web pages should go through Bugzilla::Auth::WWW.
-These packages would contain modules of their own to perform whatever extra
-functions are needed, like the CGI and Cookie modules in the case of WWW.
-
-Task: Verify
-------------
-
-This task validates user credentials against a user authentication service.
-
-The default service in Bugzilla has been the database, which stores the
-login_name and cryptpasswd fields in the profiles table.  An alternative means
-of validation, LDAP, is already supported, and other contributions would be
-appreciated.
-
-The module layout is similar to the Login package, but there is no need for a
-sub-level as there is with Login request types.
-
-Params
-------
-
-There are two params that define behaviour for each authentication task.  Each
-of them defines a comma-separated list of modules to be tried in order.
-
-    - user_info_class determines the module(s) used to obtain user
-      credentials. This param is specific to the requests from Bugzilla web
-      pages, so all of the listed modules live under
-      Bugzilla::Auth::Login::WWW
-
-    - user_verify_class determines the module(s) used to verify credentials.
-      This param is general and concerns the whole Bugzilla instance, since
-      the same back end should be used regardless of what front end is used.
-
-Responsibilities
-----------------
-
-Bugzilla::Auth
-
-    This module is responsible for abstracting away as much as possible the
-    login and logout tasks in Bugzilla.
-
-    It offers  login() and logout() methods that are proxied to the selected
-    login and verify packages.
-
-Bugzilla::Auth::Login
-
-    This is a container to hold the various modules for each request type.
-
-Bugzilla::Auth::Login::WWW
-
-    This module is responsible for abstracting away details of which web-based
-    login modules exist and are in use. It offers login() and logout() methods
-    that proxy through to whatever specific modules
-
-Bugzilla::Auth::Verify
-
-    This module is responsible for abstracting away details of which
-    credential verification modules exist, and should proxy calls through to
-    them. There is a method that is particularly important, and which should
-    be proxied through to the specific:
-
-        can_edit($type)
-
-            This method takes an argument that specifies what sort of change
-            is being requested; the specific module should return 1 or 0 based
-            on the fact that it implements or not the required change.
-
-            Current values for $type are "new" for new accounts, and "userid",
-            "login_name", "realname" for their respective fields.
-
-Specific Login Modules
-----------------------
-
-    WWW
-
-        The main authentication frontend; regular pages (CGIs) should use only
-        this module. It offers a convenient frontend to the main functionality
-        that CGIs need, using form parameters and cookies.
-
-        - Cookie
-
-            Implements part of the backend code that deals with browser
-            cookies. It's actually tied in to DB.pm, so Cookie logins that use
-            LDAP won't work at all.
-
-    LDAP
-
-        The other authentication module is LDAP-based; it is *only* used for
-        password authentication and not for any other login-related task (it
-        actually relies on the database to handle the profile information).
-
-Legacy
-------
-
-Bugzilla.pm
-
-    There is glue code that currently lives in the top-level module
-    Bugzilla.pm; this module handles backwards-compatibility data that is used
-    in a number of CGIs. This data has been slowly removed from the Bugzilla
-    pages and eventually should go away completely, at which point Bugzilla.pm
-    will be just a wrapper to conveniently offer template, cgi, dbh and user
-    variables.
-
-    This module is meant to be used only by Bugzilla pages, and in the case of
-    a reorganization which moves CGI-specific code to a subdirectory,
-    Bugzilla.pm should go with it.
-
diff --git a/Bugzilla/Auth/Verify.pm b/Bugzilla/Auth/Verify.pm
new file mode 100644 (file)
index 0000000..cbff2c7
--- /dev/null
@@ -0,0 +1,223 @@
+# -*- 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.
+#
+# Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org>
+
+package Bugzilla::Auth::Verify;
+
+use strict;
+use fields qw();
+
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::User;
+use Bugzilla::Util;
+
+use constant user_can_create_account => 1;
+
+sub new {
+    my ($class, $login_type) = @_;
+    my $self = fields::new($class);
+    return $self;
+}
+
+sub can_change_password {
+    return $_[0]->can('change_password');
+}
+
+sub create_or_update_user {
+    my ($self, $params) = @_;
+    my $dbh = Bugzilla->dbh;
+
+    my $extern_id = $params->{extern_id};
+    my $username  = $params->{bz_username} || $params->{username};
+    my $password  = $params->{password} || '*';
+    my $real_name = $params->{realname} || '';
+    my $user_id   = $params->{user_id};
+
+    # A passed-in user_id always overrides anything else, for determining
+    # what account we should return.
+    if (!$user_id) {
+        my $username_user_id = login_to_id($username || '');
+        my $extern_user_id;
+        if ($extern_id) {
+            trick_taint($extern_id);
+            $extern_user_id = $dbh->selectrow_array('SELECT userid
+                 FROM profiles WHERE extern_id = ?', undef, $extern_id);
+        }
+
+        # If we have both a valid extern_id and a valid username, and they are
+        # not the same id, then we have a conflict.
+        if ($username_user_id && $extern_user_id
+            && $username_user_id ne $extern_user_id)
+        {
+            my $extern_name = Bugzilla::User->new($extern_user_id)->login;
+            return { failure => AUTH_ERROR, error => "extern_id_conflict",
+                     details => {extern_id   => $extern_id,
+                                 extern_user => $extern_name,
+                                 username    => $username} };
+        }
+
+        # If we have a valid username, but no valid id,
+        # then we have to create the user. This happens when we're
+        # passed only a username, and that username doesn't exist already.
+        if ($username && !$username_user_id && !$extern_user_id) {
+            validate_email_syntax($username)
+              || return { failure => AUTH_ERROR, 
+                          error   => 'auth_invalid_email',
+                          details => {addr => $username} };
+            insert_new_user($username, $real_name, $password);
+            $username_user_id = login_to_id($username);
+        }
+
+        # If we have a valid username id and an extern_id, but no valid
+        # extern_user_id, then we have to set the user's extern_id.
+        if ($extern_id && $username_user_id && !$extern_user_id) {
+            $dbh->do('UPDATE profiles SET extern_id = ? WHERE userid = ?',
+                     undef, $extern_id, $username_user_id);
+        }
+
+        # Finally, at this point, one of these will give us a valid user id.
+        $user_id = $extern_user_id || $username_user_id;
+    }
+
+    # If we still don't have a valid user_id, then we weren't passed
+    # enough information in $params, and we should die right here.
+    ThrowCodeError('bad_arg', {argument => 'params', function =>
+        'Bugzilla::Auth::Verify::create_or_update_user'})
+        unless $user_id;
+
+    my $user = new Bugzilla::User($user_id);
+
+    # Now that we have a valid User, we need to see if any data has to be
+    # updated.
+    if ($username && $user->login ne $username) {
+        validate_email_syntax($username)
+          || return { failure => AUTH_ERROR, error => 'auth_invalid_email',
+                      details => {addr => $username} };
+        $dbh->do('UPDATE profiles SET login_name = ? WHERE userid = ?',
+                 $username, $user->id);
+    }
+    if ($real_name && $user->realname ne $real_name) {
+        $dbh->do('UPDATE profiles SET realname = ? WHERE userid = ?',
+                 undef, $real_name, $user->id);
+    }
+
+    return { user => $user };
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::Auth::Verify - An object that verifies usernames and passwords.
+
+=head1 DESCRIPTION
+
+Bugzilla::Auth::Verify provides the "Verifier" part of the Bugzilla 
+login process. (For details, see the "STRUCTURE" section of 
+L<Bugzilla::Auth>.)
+
+It is mostly an abstract class, requiring subclasses to implement
+most methods.
+
+Note that callers outside of the C<Bugzilla::Auth> package should never
+create this object directly. Just create a C<Bugzilla::Auth> object
+and call C<login> on it.
+
+=head1 VERIFICATION METHODS
+
+These are the methods that have to do with the actual verification.
+
+Subclasses MUST implement these methods.
+
+=over 4
+
+=item C<check_credentials($login_data)>
+
+Description: Checks whether or not a username is valid.
+Params:      $login_data - A C<$login_data> hashref, as described in
+                           L<Bugzilla::Auth>.
+                           This C<$login_data> hashref MUST contain
+                           C<username>, and SHOULD also contain
+                           C<password>.
+Returns:     A C<$login_data> hashref with C<bz_username> set. This
+             method may also set C<realname>. It must avoid changing
+             anything that is already set.
+
+=back
+
+=head1 MODIFICATION METHODS
+
+These are methods that change data in the actual authentication backend.
+
+These methods are optional, they do not have to be implemented by
+subclasses.
+
+=over 4
+
+=item C<create_or_update_user($login_data)>
+
+Description: Automatically creates a user account in the database
+             if it doesn't already exist, or updates the account
+             data if C<$login_data> contains newer information.
+
+Params:      $login_data - A C<$login_data> hashref, as described in
+                           L<Bugzilla::Auth>.
+                           This C<$login_data> hashref MUST contain
+                           either C<user_id>, C<bz_username>, or
+                           C<username>. If both C<username> and C<bz_username>
+                           are specified, C<bz_username> is used as the
+                           login name of the user to create in the database.
+                           It MAY also contain C<extern_id>, in which
+                           case it still MUST contain C<bz_username> or
+                           C<username>.
+                           It MAY contain C<password> and C<realname>.
+
+Returns:     A hashref with one element, C<user>, which is a
+             L<Bugzilla::User> object. May also return a login error
+             as described in L<Bugzilla::Auth>.
+
+Note:        This method is not abstract, it is actually implemented
+             and creates accounts in the Bugzilla database. Subclasses
+             should probably all call the C<Bugzilla::Auth::Verify>
+             version of this function at the end of their own
+             C<create_or_update_user>.
+
+=item C<change_password($user, $password)>
+
+Description: Modifies the user's password in the authentication backend.
+Params:      $user - A L<Bugzilla::User> object representing the user
+                     whose password we want to change.
+             $password - The user's new password.
+Returns:     Nothing.
+
+=back
+
+=head1 INFO METHODS
+
+These are methods that describe the capabilities of this object.
+These are all no-parameter methods that return either C<true> or 
+C<false>.
+
+=over 4
+
+=item C<user_can_create_account>
+
+Whether or not users can manually create accounts in this type of
+account source. Defaults to C<true>.
+
+=back
index 405a737b87dd6b098878036f7c7a55ba985f37a2..88ad78d5449d9b51e041296659f1407564b3d5b8 100644 (file)
 #                 Erik Stambaugh <erik@dasbistro.com>
 
 package Bugzilla::Auth::Verify::DB;
-
 use strict;
+use base qw(Bugzilla::Auth::Verify);
 
-use Bugzilla::Config;
 use Bugzilla::Constants;
+use Bugzilla::Token;
 use Bugzilla::Util;
 use Bugzilla::User;
 
-my $edit_options = {
-    'new' => 1,
-    'userid' => 0,
-    'login_name' => 1,
-    'realname' => 1,
-};
+sub check_credentials {
+    my ($self, $login_data) = @_;
+    my $dbh = Bugzilla->dbh;
 
-sub can_edit {
-    my ($class, $type) = @_;
-    return $edit_options->{$type};
-}
+    my $username = $login_data->{username};
+    my $user_id  = login_to_id($username);
 
-sub authenticate {
-    my ($class, $username, $passwd) = @_;
+    return { failure => AUTH_NO_SUCH_USER } unless $user_id;
 
-    return (AUTH_NODATA) unless defined $username && defined $passwd;
+    $login_data->{bz_username} = $username;
+    my $password = $login_data->{password};
 
-    my $userid = Bugzilla::User::login_to_id($username);
-    return (AUTH_LOGINFAILED) unless $userid;
+    trick_taint($username);
+    my ($real_password_crypted) = $dbh->selectrow_array(
+        "SELECT cryptpassword FROM profiles WHERE userid = ?",
+        undef, $user_id);
 
-    return (AUTH_LOGINFAILED, $userid) 
-        unless $class->check_password($userid, $passwd);
+    # Using the internal crypted password as the salt,
+    # crypt the password the user entered.
+    my $entered_password_crypted = crypt($password, $real_password_crypted);
+    return { failure => AUTH_LOGINFAILED }
+        if $entered_password_crypted ne $real_password_crypted;
 
     # The user's credentials are okay, so delete any outstanding
     # password tokens they may have generated.
-    require Bugzilla::Token;
-    Bugzilla::Token::DeletePasswordTokens($userid, "user_logged_in");
-
-    # Account may have been disabled
-    my $disabledtext = $class->get_disabled($userid);
-    return (AUTH_DISABLED, $userid, $disabledtext)
-      if $disabledtext ne '';
-
-    return (AUTH_OK, $userid);
-}
-
-sub get_disabled {
-    my ($class, $userid) = @_;
-    my $dbh = Bugzilla->dbh;
-    my $sth = $dbh->prepare_cached("SELECT disabledtext FROM profiles " .
-                                   "WHERE userid=?");
-    my ($text) = $dbh->selectrow_array($sth, undef, $userid);
-    return $text;
-}
-
-sub check_password {
-    my ($class, $userid, $passwd) = @_;
-    my $dbh = Bugzilla->dbh;
-    my $sth = $dbh->prepare_cached("SELECT cryptpassword FROM profiles " .
-                                   "WHERE userid=?");
-    my ($realcryptpwd) = $dbh->selectrow_array($sth, undef, $userid);
-
-    # 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);
+    Bugzilla::Token::DeletePasswordTokens($user_id, "user_logged_in");
 
-    return $enteredCryptedPassword eq $realcryptpwd;
+    return $login_data;
 }
 
 sub change_password {
-    my ($class, $userid, $password) = @_;
+    my ($self, $user, $password) = @_;
     my $dbh = Bugzilla->dbh;
     my $cryptpassword = bz_crypt($password);
-    $dbh->do("UPDATE profiles SET cryptpassword = ? WHERE userid = ?", 
-             undef, $cryptpassword, $userid);
+    $dbh->do("UPDATE profiles SET cryptpassword = ? WHERE userid = ?",
+             undef, $cryptpassword, $user->id);
 }
 
 1;
-
-__END__
-
-=head1 NAME
-
-Bugzilla::Auth::Verify::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>
index 376fac71db9a169ea075d578a9ffd7942a8fd57e..8480185499c673b0982169d9fa9903a35275a079 100644 (file)
 #                 Christian Reis <kiko@async.com.br>
 #                 Bradley Baetz <bbaetz@acm.org>
 #                 Erik Stambaugh <erik@dasbistro.com>
+#                 Max Kanat-Alexander <mkanat@bugzilla.org>
 
 package Bugzilla::Auth::Verify::LDAP;
-
 use strict;
+use base qw(Bugzilla::Auth::Verify);
+use fields qw(
+    ldap
+);
 
 use Bugzilla::Config;
 use Bugzilla::Constants;
-use Bugzilla::User;
+use Bugzilla::Error;
 
 use Net::LDAP;
 
-my $edit_options = {
-    'new' => 0,
-    'userid' => 0,
-    'login_name' => 0,
-    'realname' => 0,
-};
-
-sub can_edit {
-    my ($class, $type) = @_;
-    return $edit_options->{$type};
-}
+use constant DEFAULT_PORT     => 389;
+use constant DEFAULT_SSL_PORT => 636;
 
-sub authenticate {
-    my ($class, $username, $passwd) = @_;
+use constant admin_can_create_account => 0;
+use constant user_can_create_account  => 0;
 
-    # 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;
+sub check_credentials {
+    my ($self, $params) = @_;
+    my $dbh = Bugzilla->dbh;
 
     # We need to bind anonymously to the LDAP server.  This is
     # because we need to get the Distinguished Name of the user trying
@@ -67,151 +58,108 @@ sub authenticate {
     # 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");
+    $self->_bind_ldap_anonymously();
+
+    # Now, we verify that the user exists, and get a LDAP Distinguished
+    # Name for the user.
+    my $username = $params->{username};
+    my $dn_result = $self->ldap->search(_bz_search_params($username),
+                                        attrs  => ['dn']);
+    return { failure => AUTH_ERROR, error => "ldap_search_error",
+             details => {errstr => $dn_result->error, username => $username}
+    } if $dn_result->code;
+
+    return { failure => AUTH_NO_SUCH_USER } if !$dn_result->count;
+
+    my $dn = $dn_result->shift_entry->dn;
+
+    # Check the password.   
+    my $pw_result = $self->ldap->bind($dn, password => $params->{password});
+    return { failure => AUTH_LOGINFAILED } if $pw_result->code;
+
+    # And now we fill in the user's details.
+    my $detail_result = $self->ldap->search(_bz_search_params($username));
+    return { failure => AUTH_ERROR, error => "ldap_search_error",
+             details => {errstr => $detail_result->error, username => $username}
+    } if $detail_result->code;
+
+    my $user_entry = $detail_result->shift_entry;
+
+    my $mail_attr = Param("LDAPmailattribute");
+    if (!$user_entry->exists($mail_attr)) {
+        return { failure => AUTH_ERROR,
+                 error   => "ldap_cannot_retreive_attr",
+                 details => {attr => $mail_attr} };
     }
 
-    my $LDAPport = "389";  # default LDAP port
-    my $LDAPprotocol = "ldap";
-
-    if ($LDAPserver =~ /(ldap|ldaps):\/\/(.*)/) {
-        # ldap(s)://server(:port)
-        $LDAPprotocol = $1;
-        my $serverpart = $2;
-        if ($serverpart =~ /:/) {
-            # ldap(s)://server:port
-            ($LDAPserver, $LDAPport) = split(":", $serverpart);
-        } else {
-            # ldap(s)://server
-            $LDAPserver = $serverpart;
-            if ($LDAPprotocol eq "ldaps") {
-                $LDAPport = "636";
-            }
-        }
-    } elsif ($LDAPserver =~ /:/) {
-        # server:port
-        ($LDAPserver, $LDAPport) = split(":", $LDAPserver);
-    }
+    $params->{bz_username} = $user_entry->get_value($mail_attr);
+    $params->{realname}  ||= $user_entry->get_value("displayName");
+    $params->{realname}  ||= $user_entry->get_value("cn");
 
+    return $params;
+}
 
-    my $LDAPconn = Net::LDAP->new("$LDAPprotocol://$LDAPserver:$LDAPport", version => 3);
-    if(!$LDAPconn) {
-        return (AUTH_ERROR, undef, "connect_failed");
-    }
+sub _bz_search_params {
+    my ($username) = @_;
+    return (base   => Param("LDAPBaseDN"),
+            scope  => "sub",
+            filter => '(&(' . Param("LDAPuidattribute") . "=$username)"
+                      . Param("LDAPfilter") . ')');
+}
 
-    my $mesg;
+sub _bind_ldap_anonymously {
+    my ($self) = @_;
+    my $bind_result;
     if (Param("LDAPbinddn")) {
         my ($LDAPbinddn,$LDAPbindpass) = split(":",Param("LDAPbinddn"));
-        $mesg = $LDAPconn->bind($LDAPbinddn, password => $LDAPbindpass);
+        $bind_result = 
+            $self->ldap->bind($LDAPbinddn, password => $LDAPbindpass);
     }
     else {
-        $mesg = $LDAPconn->bind();
-    }
-    if($mesg->code) {
-        return (AUTH_ERROR, undef,
-                "connect_failed",
-                { errstr => $mesg->error });
+        $bind_result = $self->ldap->bind();
     }
+    ThrowCodeError("ldap_bind_failed", {errstr => $bind_result->error})
+        if $bind_result->code;
+}
 
-    # We've got our anonymous bind;  let's look up this user.
-    $mesg = $LDAPconn->search( base   => Param("LDAPBaseDN"),
-                               scope  => "sub",
-                               filter => '(&(' . Param("LDAPuidattribute") . "=$username)" . Param("LDAPfilter") . ')',
-                               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)" . Param("LDAPfilter") . ')',
-                             );
-    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") });
-    }
+# We can't just do this in new(), because we're not allowed to throw any
+# error from anywhere under Bugzilla::Auth::new -- otherwise we
+# could create a situation where the admin couldn't get to editparams
+# to fix his mistake. (Because Bugzilla->login always calls 
+# Bugzilla::Auth->new, and almost every page calls Bugzilla->login.)
+sub ldap {
+    my ($self) = @_;
+    return $self->{ldap} if $self->{ldap};
 
-    # 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
+    my $server = Param("LDAPserver");
+    ThrowCodeError("ldap_server_not_defined") unless $server;
 
-    # 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 $port = DEFAULT_PORT;
+    my $protocol = "ldap";
 
-    my $dbh = Bugzilla->dbh;
-    my $sth = $dbh->prepare_cached("SELECT userid, disabledtext " .
-                                   "FROM profiles " .
-                                   "WHERE " .
-                                   $dbh->sql_istrcmp('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");
+    if ($server =~ /(ldap|ldaps):\/\/(.*)/) {
+        # ldap(s)://server(:port)
+        $protocol = $1;
+        my $server_part = $2;
+        if ($server_part =~ /:/) {
+            # ldap(s)://server:port
+            ($server, $port) = split(":", $server_part);
+        } else {
+            # ldap(s)://server
+            $server = $server_part;
+            if ($protocol eq "ldaps") {
+                $port = DEFAULT_SSL_PORT;
+            }
         }
-        insert_new_user($username, $userRealName);
-
-        ($userid, $disabledtext) = $dbh->selectrow_array($sth,
-                                                         undef,
-                                                         $username);
-        return (AUTH_ERROR, $userid, "no_userid")
-          unless $userid;
+    } elsif ($server =~ /:/) {
+        # server:port
+        ($server, $port) = split(":", $server);
     }
 
-    # 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);
+    my $conn_string = "$protocol://$server:$port";
+    $self->{ldap} = new Net::LDAP($conn_string) 
+        || ThrowCodeError("ldap_connect_failed", { server => $conn_string });
+    return $self->{ldap};
 }
 
 1;
-
-__END__
-
-=head1 NAME
-
-Bugzilla::Auth::Verify::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:E<sol>E<sol>bugzilla.mozilla.orgE<sol>> 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/Auth/Verify/Stack.pm b/Bugzilla/Auth/Verify/Stack.pm
new file mode 100644 (file)
index 0000000..577b5a2
--- /dev/null
@@ -0,0 +1,81 @@
+# -*- 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.
+#
+# Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org>
+
+package Bugzilla::Auth::Verify::Stack;
+use strict;
+use base qw(Bugzilla::Auth::Verify);
+use fields qw(
+    _stack
+    successful
+);
+
+sub new {
+    my $class = shift;
+    my $list = shift;
+    my $self = $class->SUPER::new(@_);
+    $self->{_stack} = [];
+    foreach my $verify_method (split(',', $list)) {
+        require "Bugzilla/Auth/Verify/${verify_method}.pm";
+        push(@{$self->{_stack}}, 
+             "Bugzilla::Auth::Verify::$verify_method"->new(@_));
+    }
+    return $self;
+}
+
+sub can_change_password {
+    my ($self) = @_;
+    # We return true if any method can change passwords.
+    foreach my $object (@{$self->{_stack}}) {
+        return 1 if $object->can_change_password;
+    }
+    return 0;
+}
+
+sub check_credentials {
+    my $self = shift;
+    my $result;
+    foreach my $object (@{$self->{_stack}}) {
+        $result = $object->check_credentials(@_);
+        $self->{successful} = $object;
+        last if !$result->{failure};
+        # So that if none of them succeed, it's undef.
+        $self->{successful} = undef;
+    }
+    # Returns the result at the bottom of the stack if they all fail.
+    return $result;
+}
+
+sub create_or_update_user {
+    my $self = shift;
+    my $result;
+    foreach my $object (@{$self->{_stack}}) {
+        $result = $object->create_or_update_user(@_);
+        last if !$result->{failure};
+    }
+    # Returns the result at the bottom of the stack if they all fail.
+    return $result;
+}
+
+sub user_can_create_account {
+    my ($self) = @_;
+    # We return true if any method allows the user to create an account.
+    foreach my $object (@{$self->{_stack}}) {
+        return 1 if $object->user_can_create_account;
+    }
+    return 0;
+}
+
+1;
index 8e245d0b6761d0cd6dbf3d76e5361b95397f047f..aed574f6708d4d517bde513990a3e17da9c80e5a 100644 (file)
@@ -43,6 +43,7 @@ use base qw(Exporter);
     AUTH_ERROR
     AUTH_LOGINFAILED
     AUTH_DISABLED
+    AUTH_NO_SUCH_USER
 
     USER_PASSWORD_MIN_LENGTH
     USER_PASSWORD_MAX_LENGTH
@@ -144,6 +145,7 @@ use constant AUTH_NODATA => 1;
 use constant AUTH_ERROR => 2;
 use constant AUTH_LOGINFAILED => 3;
 use constant AUTH_DISABLED => 4;
+use constant AUTH_NO_SUCH_USER  => 5;
 
 # The minimum and maximum lengths a password must have.
 use constant USER_PASSWORD_MIN_LENGTH => 3;
index 4fb41d852df6171045c125748f81ffa42bfa4413..93316bdb27d88130dec57bb4f3622796c55d1da0 100644 (file)
@@ -155,17 +155,17 @@ sub disabledtext { $_[0]->{'disabledtext'}; }
 sub is_disabled { $_[0]->disabledtext ? 1 : 0; }
 sub showmybugslink { $_[0]->{showmybugslink}; }
 
-sub set_flags {
-    my $self = shift;
-    while (my $key = shift) {
-        $self->{'flags'}->{$key} = shift;
-    }
+sub set_authorizer {
+    my ($self, $authorizer) = @_;
+    $self->{authorizer} = $authorizer;
 }
-
-sub get_flag {
-    my $self = shift;
-    my $key = shift;
-    return $self->{'flags'}->{$key};
+sub authorizer {
+    my ($self) = @_;
+    if (!$self->{authorizer}) {
+        require Bugzilla::Auth;
+        $self->{authorizer} = new Bugzilla::Auth();
+    }
+    return $self->{authorizer};
 }
 
 # Generate a string to identify the user by name + login if the user
@@ -1505,6 +1505,17 @@ which to identify the user. Currently the part of the user's email address
 before the at sign (@), but that could change, especially if we implement
 usernames not dependent on email address.
 
+=item C<authorizer>
+
+This is the L<Bugzilla::Auth> object that the User logged in with.
+If the user hasn't logged in yet, a new, empty Bugzilla::Auth() object is
+returned.
+
+=item C<set_authorizer($authorizer)>
+
+Sets the L<Bugzilla::Auth> object to be returned by C<authorizer()>.
+Should only be called by C<Bugzilla::Auth::login>, for the most part.
+
 =item C<queries>
 
 Returns an array of the user's named queries, sorted in a case-insensitive
@@ -1718,21 +1729,6 @@ When called with one argument:
 Returns C<1> if the user can bless the group with that name, returns
 C<0> otherwise.
 
-=item C<set_flags>
-=item C<get_flag>
-
-User flags are template-accessible user status information, stored in the form
-of a hash.  For an example of use, when the current user is authenticated in
-such a way that they are allowed to log out, the 'can_logout' flag is set to
-true (1).  The template then checks this flag before displaying the "Log Out"
-link.
-
-C<set_flags> is called with any number of key,value pairs.  Flags for each key
-will be set to the specified value.
-
-C<get_flag> is called with a single key name, which returns the associated
-value.
-
 =item C<wants_bug_mail>
 
 Returns true if the user wants mail for a given bug change.
index 01b746c89cffca4cd29eba67df8943a029d92256..edd1c0a66dc546abdbc73208db42c8304749a498 100755 (executable)
@@ -49,7 +49,7 @@ my $vars = {};
 print $cgi->header();
 
 # If we're using LDAP for login, then we can't create a new account here.
-unless (Bugzilla::Auth->can_edit('new')) {
+unless (Bugzilla->user->authorizer->user_can_create_account) {
     ThrowUserError("auth_cant_create_account");
 }
 
index 7286a4b2ea593ffd5ea04fe7152515a0dfd4365b..8afb327e93276ffd9e5d91cc38fb14827a792fa8 100755 (executable)
--- a/query.cgi
+++ b/query.cgi
@@ -53,15 +53,7 @@ my $template = Bugzilla->template;
 my $vars = {};
 my $buffer = $cgi->query_string();
 
-if ($cgi->param("GoAheadAndLogIn")) {
-    # We got here from a login page, probably from relogin.cgi.  We better
-    # make sure the password is legit.
-    Bugzilla->login(LOGIN_REQUIRED);
-} else {
-    Bugzilla->login();
-}
-
-my $user = Bugzilla->user;
+my $user = Bugzilla->login();
 my $userid = $user->id;
 
 # Backwards compatibility hack -- if there are any of the old QUERY_*
index 0d772f9eb9cf9ed9ed77ac9125ae3e5cfa39af89..0385b8e2fb26f9af80529ad5a10a71493b19e17c 100755 (executable)
@@ -75,8 +75,7 @@ elsif ($action eq 'begin-sudo') {
     # and password.
     # We only need to do this for authentication methods that involve Bugzilla 
     # directly obtaining a login (i.e. normal CGI login), as opposed to other 
-    # methods (like Environment vars login).  We assume that if a user can log 
-    # out, they can also log in:
+    # methods (like Environment vars login). 
 
     # First, record if Bugzilla_login and Bugzilla_password were provided
     my $credentials_provided;
@@ -92,7 +91,7 @@ elsif ($action eq 'begin-sudo') {
     # At this point, the user is logged in.  However, if they used a method
     # where they could have provided a username/password (i.e. CGI), but they 
     # did not provide a username/password, then throw an error.
-    if ($user->get_flag('can_logout') && !$credentials_provided) {
+    if ($user->authorizer->can_login && !$credentials_provided) {
         ThrowUserError('sudo_password_required',
                        { target_login => $cgi->param('target_login'),
                                reason => $cgi->param('reason')});
index 7f846efeaef4fcf233b46d02d338e63beff61fa8..36e32505776cb779574e23d1d69956e0099336f0 100755 (executable)
@@ -37,11 +37,7 @@ my $cgi = Bugzilla->cgi;
 my $template = Bugzilla->template;
 my $vars = {};
 
-if ($cgi->param('GoAheadAndLogIn')) {
-    Bugzilla->login(LOGIN_REQUIRED);
-} else {
-    Bugzilla->login();
-}
+Bugzilla->login();
 
 # Editable, 'single' HTML bugs are treated slightly specially in a few places
 my $single = !$cgi->param('format')
diff --git a/template/en/default/account/auth/ldap-error.html.tmpl b/template/en/default/account/auth/ldap-error.html.tmpl
deleted file mode 100644 (file)
index dba4315..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-[%# 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.
-  #%]
-
-[% PROCESS global/variables.none.tmpl %]
-
-[% admindocslinks = {'extraconfig.html#bzldap' => 'Setting up LDAP authentication'} %]
-
-[% 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" %]
-    [% terms.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: <tt>[% auth_err_tag FILTER html %]</tt>
-
-[% END %]
index a18ee70a2f66e779f328fcc12f51ca1db1ba0326..ca500a8aac44705b8068595f67f610bc9b2e3afb 100644 (file)
@@ -79,7 +79,7 @@
       [%# For now, password change requests only apply to the DB
         # verification method #%]
 
-      [% IF has_db != 0 %]
+      [% IF user.authorizer.can_change_password %]
         <td>[ <a href="index.cgi?GoAheadAndLogIn=1#forgot">Forgot my Password</a> ]</td>
       [% END %]
     </tr>
index d2c4950e4b1035e091775fbd6a3c2429b9b02ecd..adbae970a5bbc122a083f3ea0b369601e40ff36d 100644 (file)
@@ -22,7 +22,6 @@
 
 [%# INTERFACE:
   # target: string. URL to go to after login.
-  # has_db: true if DB is one of the available authentication mechanisms
   #%]
 
 [% PROCESS global/variables.none.tmpl %]
   # their password, assuming that our auth method allows that.
   #%]
 
-[% IF caneditaccount %]
-
-  [% IF Param("createemailregexp") %]
+  [% IF Param("createemailregexp") && user.authorizer.user_can_create_account %]
     <hr>
 
     <p>
     </p>
   [% END %]
 
-  [%# For now, password change requests only apply to the DB
-    # verification method #%]
-
-  [% IF has_db != 0 %]
+  [% IF user.authorizer.can_change_password %]
     <hr>
 
     <a name="forgot"></a>
       <input size="35" name="loginname">
       <input type="submit" value="Submit Request">
     </form>
-
   [% END %]
 
-  <hr>
-[% END %]
-
 [% PROCESS global/footer.html.tmpl %]
index dc936d43cbdd4a0f4cc43cda5ab64c16d87e5d5c..e914ecf6cafa60ca3e938f1472c093636a64d88b 100644 (file)
   <tr>
     <td colspan="2"><hr></td>
   </tr>
+  [% IF user.authorizer.can_change_password %]
+    <tr>
+      <th align="right">New password:</th>
+      <td>
+        <input type="password" name="new_password1">
+      </td>
+    </tr>              
 
-  <tr>
-    <th align="right">New password:</th>
-    <td>
-      <input type="password" name="new_password1">
-    </td>
-  </tr>              
-
-  <tr>
-    <th align="right">Re-enter new password:</th>
-    <td>
-      <input type="password" name="new_password2">
-    </td>
-  </tr>              
+    <tr>
+      <th align="right">Re-enter new password:</th>
+      <td>
+        <input type="password" name="new_password2">
+      </td>
+    </tr>              
+  [% END %]
 
   <tr>
     <th align="right">Your real name (optional, but encouraged):</th>
@@ -64,7 +65,7 @@
     </td>
   </tr>  
 
-  [% IF Param('allowemailchange') %]
+  [% IF user.authorizer.can_change_email && Param('allowemailchange') %]
     [% IF login_change_date %]
       [% IF new_login_name %]
         <tr>
index 4e781796c32eed3c7fbedda359d6e6aee6b53d2f..b353905450ae49234eafb6b41d475292ed717640 100644 (file)
@@ -76,7 +76,7 @@
     are impersonating them.
   </p>
   
-  [% IF user.get_flag("can_logout") %]
+  [% IF user.authorizer.can_login %]
     <p>
       Finally, enter your [% terms.Bugzilla %] password: 
       <input type="hidden" name="Bugzilla_login" value="
index d9220f27b17f20f7f12f15033b96314e1f0f00d9..1c15255af1a961fb3740d19fdbf0ab5863309bcc 100644 (file)
     Attachment #[% attach_id 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 == "auth_invalid_email" %]
+    [% title = "Invalid Email Address" %]
+    We received an email address (<b>[% addr FILTER html %]</b>)
+    that didn't pass our syntax checking for a legal email address,
+    when trying to create or update your account.
+    [% IF default %]
+      A legal address must contain exactly one '@',
+      and at least one '.' after the @.
+    [% ELSE %]
+      [%+ Param('emailregexpdesc') %]
+    [% END %]
+    It must also not contain any of these special characters:
+    <tt>\ ( ) &amp; &lt; &gt; , ; : &quot; [ ]</tt>, or any whitespace.</tt>
 
   [% ELSIF error == "authres_unhandled" %]
-    An authorization handler return value was not handled by the login code.
+    The result value of [% value FILTER html %] was not handled by
+    the login code.
 
   [% ELSIF error == "bad_page_cgi_id" %]
     [% title = "Invalid Page ID" %]
   [% ELSIF error == "cookies_need_value" %]
     Every cookie must have a value.
 
+  [% ELSIF error == "env_no_email" %]
+    [% terms.Bugzilla %] did not receive an email address from the 
+    environment.
+    [% IF Param("auth_env_email") %]
+      This means that the '[% Param("auth_env_email") FILTER html %]'
+      environment variable was empty or did not exist.
+    [% ELSE %]
+      You need to set the "auth_env_email" environment variable to
+      the name of the environment variable that will contain the
+      user's email address.
+    [% END %]
+
   [% ELSIF error == "extension_invalid" %]
     An error occured processing hook [% name FILTER html %] in
     extension [% extension FILTER html %].
 
+  [% ELSIF error == "extern_id_conflict" %]
+    The external ID '[% extern_id FILTER html %]' already exists
+    in the database for '[% username FILTER html %]', but your
+    account source says that '[% extern_user FILTER html %]' has that ID.
+
   [% ELSIF error == "field_type_mismatch" %]
     Cannot seem to handle <code>[% field FILTER html %]</code>
     and <code>[% type FILTER html %]</code> together.
       given.
     [% END %]
 
+  [% ELSIF error == "ldap_bind_failed" %]
+    Failed to bind to the LDAP server. The error message was: 
+    <code>[% errstr FILTER html %]</code>
+
+  [% ELSIF error == "ldap_cannot_retreive_attr" %]
+    The specified LDAP attribute [% attr FILTER html %] was not found.
+
+  [% ELSIF error == "ldap_connect_failed" %]
+    Could not connect to the LDAP server <code>[% server FILTER html %]</code>.
+
+  [% ELSIF error == "ldap_server_not_defined" %]
+    The LDAP server for authentication has not been defined.
+
   [% ELSIF error == "missing_bug_id" %]
     No [% terms.bug %] ID was given.
     
index b2e598163f604192166172c904a3f7044e700d0a..3ac44d62a7f5e73babcb817489cc77fda5506fcf 100644 (file)
@@ -57,7 +57,7 @@
         [% IF user.login %] 
             [% ' | <a href="sanitycheck.cgi">Sanity&nbsp;check</a>' 
                                                   IF user.groups.tweakparams %]
-            [% IF user.get_flag('can_logout') %]
+            [% IF user.authorizer.can_logout %]
               | <a href="relogin.cgi">Log&nbsp;out</a>&nbsp;
             [% ELSE %]
               | Logged&nbsp;in&nbsp;as&nbsp;
               [% user.login FILTER html %]
             [% END %]
         [% ELSE %]
-            [% IF Param('createemailregexp') %]
+            [% IF Param('createemailregexp')
+                  && user.authorizer.user_can_create_account %] 
               | <a href="createaccount.cgi">New&nbsp;Account</a>
             [% END %]
-            | <a href="index.cgi?GoAheadAndLogIn=1">Log&nbsp;In</a>
+            [% IF user.authorizer.can_login %]
+              | <a href="index.cgi?GoAheadAndLogIn=1">Log&nbsp;In</a>
+            [% END %]
         [% END %]
     </div>
   </div>
index e12bdf032195cf6192f2a6f39ed2f562468129ff..ee7f6c17dedfa57be42eebbf23310f1f28f87398 100644 (file)
     [% title = "Wrong Token" %]
     That token cannot be used to change your password.
 
-  [% ELSIF error == "extern_id_conflict" %]
-    [% title = "Extern ID Conflict" %]
-    Someone with a different external ID has that address.
-
   [% ELSIF error == "wrong_token_for_confirming_email_change" %]
     [% title = "Wrong Token" %]
     That token cannot be used to change your email address.
index a19334c3040d9d01cc60d8b70302fc9c2ef37279..c8474494d3b81e1d08bb776bbc30b6eb9bbbfa09 100644 (file)
@@ -69,12 +69,12 @@ function addSidebar() {
     <li id="report"><a href="report.cgi">Summary reports and charts</a></li>
 [% IF user.id %]
     <li id="userprefs"><a href="userprefs.cgi">Change password or user preferences</a></li>
-  [% IF user.get_flag('can_logout') %]
+  [% IF user.authorizer.can_logout %]
     <li id="logout"><a href="relogin.cgi">Log out [% user.login FILTER html %]</a></li>
   [% END %]
-[% ELSE %]
+[% ELSIF user.authorizer.can_login %]
     [% PROCESS "account/auth/login-small.html.tmpl" %]
-  [% IF Param('createemailregexp') %]
+  [% IF Param('createemailregexp') && user.authorizer.user_can_create_account %]
     <li id="account"><a href="createaccount.cgi">Open a new [% terms.Bugzilla %] account</a></li>
   [% END %]
 [% END %]
index 18f8e757334fee213074598aa9384aa9870dbed8..9ad50e07bf48b21a0d32698605865203537e57af 100644 (file)
@@ -97,7 +97,7 @@ function normal_keypress_handler( aEvent ) {
   [%- IF user.groups.tweakparams %]
       <text class="text-link" onclick="load_relative_url('sanitycheck.cgi')" value="sanity check"/>
   [%- END %]
-  [%- IF user.get_flag('can_logout') %]
+  [%- IF user.authorizer.can_logout %]
       <text class="text-link" onclick="load_relative_url('relogin.cgi')" value="log out [% user.login FILTER html %]"/>
   [%- END %]
       <separator class="thin"/>
index cbb502c67441bd5435846b9a2a515fe2dc75af34..bb253db60d72468c2e571563983e5f159198278c 100755 (executable)
--- a/token.cgi
+++ b/token.cgi
@@ -104,7 +104,7 @@ if ( $::action eq 'reqpw' ) {
       || ThrowUserError("login_needed_for_password_change");
 
     # check verification methods
-    unless (Bugzilla::Auth->has_db) {
+    unless (Bugzilla->user->authorizer->can_change_password) {
         ThrowUserError("password_change_requests_not_allowed");
     }
 
index 7eb78af42c129cae09f7f662df812ce269f029b5..9fb9c6614aa1a3292a2f3241be45dac724edeb95 100755 (executable)
@@ -50,7 +50,8 @@ sub DoAccount {
     ($vars->{'realname'}) = $dbh->selectrow_array(
         "SELECT realname FROM profiles WHERE userid = ?", undef, $user->id);
 
-    if(Param('allowemailchange')) {
+    if(Param('allowemailchange') 
+       && Bugzilla->user->authorizer->can_change_email) {
         my @token = $dbh->selectrow_array(
             "SELECT tokentype, issuedate + " .
                     $dbh->sql_interval(3, 'DAY') . ", eventdata