diff options
author | mkanat%bugzilla.org <> | 2006-05-12 11:40:56 +0200 |
---|---|---|
committer | mkanat%bugzilla.org <> | 2006-05-12 11:40:56 +0200 |
commit | d9cbb0f0a62bba345ed26ac68364bb441f41d35d (patch) | |
tree | 415d30523fb728a3192970a6d2b168b095f260dc /Bugzilla | |
parent | d7447bf95827d7e9da681d496a192fffbc2810a4 (diff) | |
download | bugzilla-d9cbb0f0a62bba345ed26ac68364bb441f41d35d.tar.gz bugzilla-d9cbb0f0a62bba345ed26ac68364bb441f41d35d.tar.xz |
Bug 300410: Bugzilla::Auth needs to be restructured to not require a BEGIN block
Patch By Max Kanat-Alexander <mkanat@bugzilla.org> r=LpSolit, a=myk
Diffstat (limited to 'Bugzilla')
-rw-r--r-- | Bugzilla/Auth.pm | 575 | ||||
-rw-r--r-- | Bugzilla/Auth/Login.pm | 125 | ||||
-rw-r--r-- | Bugzilla/Auth/Login/CGI.pm | 73 | ||||
-rw-r--r-- | Bugzilla/Auth/Login/Cookie.pm | 83 | ||||
-rw-r--r-- | Bugzilla/Auth/Login/Env.pm | 54 | ||||
-rw-r--r-- | Bugzilla/Auth/Login/Stack.pm | 87 | ||||
-rw-r--r-- | Bugzilla/Auth/Login/WWW.pm | 111 | ||||
-rw-r--r-- | Bugzilla/Auth/Login/WWW/CGI.pm | 275 | ||||
-rw-r--r-- | Bugzilla/Auth/Login/WWW/CGI/Cookie.pm | 113 | ||||
-rw-r--r-- | Bugzilla/Auth/Login/WWW/Env.pm | 156 | ||||
-rw-r--r-- | Bugzilla/Auth/Persist/Cookie.pm | 153 | ||||
-rw-r--r-- | Bugzilla/Auth/README | 132 | ||||
-rw-r--r-- | Bugzilla/Auth/Verify.pm | 223 | ||||
-rw-r--r-- | Bugzilla/Auth/Verify/DB.pm | 96 | ||||
-rw-r--r-- | Bugzilla/Auth/Verify/LDAP.pm | 248 | ||||
-rw-r--r-- | Bugzilla/Auth/Verify/Stack.pm | 81 | ||||
-rw-r--r-- | Bugzilla/Constants.pm | 2 | ||||
-rw-r--r-- | Bugzilla/User.pm | 46 |
18 files changed, 1406 insertions, 1227 deletions
diff --git a/Bugzilla/Auth.pm b/Bugzilla/Auth.pm index 4ea3d5bd6..d650658f5 100644 --- a/Bugzilla/Auth.pm +++ b/Bugzilla/Auth.pm @@ -19,40 +19,181 @@ # # 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 index 000000000..4a4c5f26d --- /dev/null +++ b/Bugzilla/Auth/Login.pm @@ -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 index 000000000..14b64ee79 --- /dev/null +++ b/Bugzilla/Auth/Login/CGI.pm @@ -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 index 000000000..e4cc0daac --- /dev/null +++ b/Bugzilla/Auth/Login/Cookie.pm @@ -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 index 000000000..fda71bf35 --- /dev/null +++ b/Bugzilla/Auth/Login/Env.pm @@ -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 index 000000000..d51003861 --- /dev/null +++ b/Bugzilla/Auth/Login/Stack.pm @@ -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 index 29cc7fced..000000000 --- a/Bugzilla/Auth/Login/WWW.pm +++ /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 index 5030691e3..000000000 --- a/Bugzilla/Auth/Login/WWW/CGI.pm +++ /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 index c2244d15d..000000000 --- a/Bugzilla/Auth/Login/WWW/CGI/Cookie.pm +++ /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 index f437bf06f..000000000 --- a/Bugzilla/Auth/Login/WWW/Env.pm +++ /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 index 000000000..ce59ef4bd --- /dev/null +++ b/Bugzilla/Auth/Persist/Cookie.pm @@ -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 index e573e2c0b..000000000 --- a/Bugzilla/Auth/README +++ /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 index 000000000..cbff2c73f --- /dev/null +++ b/Bugzilla/Auth/Verify.pm @@ -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 diff --git a/Bugzilla/Auth/Verify/DB.pm b/Bugzilla/Auth/Verify/DB.pm index 405a737b8..88ad78d54 100644 --- a/Bugzilla/Auth/Verify/DB.pm +++ b/Bugzilla/Auth/Verify/DB.pm @@ -28,97 +28,51 @@ # 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> diff --git a/Bugzilla/Auth/Verify/LDAP.pm b/Bugzilla/Auth/Verify/LDAP.pm index 376fac71d..848018549 100644 --- a/Bugzilla/Auth/Verify/LDAP.pm +++ b/Bugzilla/Auth/Verify/LDAP.pm @@ -26,39 +26,30 @@ # 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 index 000000000..577b5a22f --- /dev/null +++ b/Bugzilla/Auth/Verify/Stack.pm @@ -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; diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm index 8e245d0b6..aed574f67 100644 --- a/Bugzilla/Constants.pm +++ b/Bugzilla/Constants.pm @@ -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; diff --git a/Bugzilla/User.pm b/Bugzilla/User.pm index 4fb41d852..93316bdb2 100644 --- a/Bugzilla/User.pm +++ b/Bugzilla/User.pm @@ -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. |