diff options
Diffstat (limited to 'Bugzilla/Auth.pm')
-rw-r--r-- | Bugzilla/Auth.pm | 543 |
1 files changed, 543 insertions, 0 deletions
diff --git a/Bugzilla/Auth.pm b/Bugzilla/Auth.pm new file mode 100644 index 0000000..09a2c1d --- /dev/null +++ b/Bugzilla/Auth.pm @@ -0,0 +1,543 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::Auth; + +use strict; +use fields qw( + _info_getter + _verifier + _persister +); + +use Bugzilla::Constants; +use Bugzilla::Error; +use Bugzilla::Mailer; +use Bugzilla::Util qw(datetime_from); +use Bugzilla::User::Setting (); +use Bugzilla::Auth::Login::Stack; +use Bugzilla::Auth::Verify::Stack; +use Bugzilla::Auth::Persist::Cookie; +use Socket; + +sub new { + my ($class, $params) = @_; + my $self = fields::new($class); + + $params ||= {}; + $params->{Login} ||= Bugzilla->params->{'user_info_class'} . ',Cookie'; + $params->{Verify} ||= Bugzilla->params->{'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; +} + +sub login { + my ($self, $type) = @_; + my $dbh = Bugzilla->dbh; + + # 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); + } + $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->is_enabled) { + 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) = @_; + my $getter = $self->{_info_getter}->{successful}; + $getter = $self->{_info_getter} + if (!$getter || $getter->isa('Bugzilla::Auth::Login::Cookie')); + return $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; +} + +sub login_token { + my ($self) = @_; + my $getter = $self->{_info_getter}->{successful}; + if ($getter && $getter->isa('Bugzilla::Auth::Login::Cookie')) { + return $getter->login_token; + } + return undef; +} + +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; +} + +sub extern_id_used { + my ($self) = @_; + return $self->{_info_getter}->extern_id_used + || $self->{_verifier}->extern_id_used; +} + +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) { + # We don't persist logins over GET requests in the WebService, + # because the persistance information can't be re-used again. + # (See Bugzilla::WebService::Server::JSONRPC for more info.) + if ($self->{_info_getter}->{successful}->requires_persistence + and !Bugzilla->request_cache->{auth_no_automatic_login}) + { + $user->{_login_token} = $self->{_persister}->persist_login($user); + } + } + elsif ($fail_code == AUTH_ERROR) { + if ($result->{user_error}) { + ThrowUserError($result->{user_error}, $result->{details}); + } + else { + ThrowCodeError($result->{error}, $result->{details}); + } + } + elsif ($fail_code == AUTH_NODATA) { + $self->{_info_getter}->fail_nodata($self) + if $login_type == LOGIN_REQUIRED; + + # If we're not LOGIN_REQUIRED, 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) + elsif ($fail_code == AUTH_LOGINFAILED or $fail_code == AUTH_NO_SUCH_USER) { + my $remaining_attempts = MAX_LOGIN_ATTEMPTS + - ($result->{failure_count} || 0); + ThrowUserError("invalid_username_or_password", + { remaining => $remaining_attempts }); + } + # The account may be disabled + elsif ($fail_code == 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}); + } + elsif ($fail_code == AUTH_LOCKOUT) { + my $attempts = $user->account_ip_login_failures; + + # We want to know when the account will be unlocked. This is + # determined by the 5th-from-last login failure (or more/less than + # 5th, if MAX_LOGIN_ATTEMPTS is not 5). + my $determiner = $attempts->[scalar(@$attempts) - MAX_LOGIN_ATTEMPTS]; + my $unlock_at = datetime_from($determiner->{login_time}, + Bugzilla->local_timezone); + $unlock_at->add(minutes => LOGIN_LOCKOUT_INTERVAL); + + # If we were *just* locked out, notify the maintainer about the + # lockout. + if ($result->{just_locked_out}) { + # We're sending to the maintainer, who may be not a Bugzilla + # account, but just an email address. So we use the + # installation's default language for sending the email. + my $default_settings = Bugzilla::User::Setting::get_defaults(); + my $template = Bugzilla->template_inner( + $default_settings->{lang}->{default_value}); + my $address = $attempts->[0]->{ip_addr}; + # Note: inet_aton will only resolve IPv4 addresses. + # For IPv6 we'll need to use inet_pton which requires Perl 5.12. + my $n = inet_aton($address); + if ($n) { + $address = gethostbyaddr($n, AF_INET) . " ($address)" + } + my $vars = { + locked_user => $user, + attempts => $attempts, + unlock_at => $unlock_at, + address => $address, + }; + my $message; + $template->process('email/lockout.txt.tmpl', $vars, \$message) + || ThrowTemplateError($template->error); + MessageToMTA($message); + } + + $unlock_at->set_time_zone($user->timezone); + ThrowUserError('account_locked', + { ip_addr => $determiner->{ip_addr}, unlock_at => $unlock_at }); + } + # If we get here, then we've run out of options, which shouldn't happen. + else { + ThrowCodeError("authres_unhandled", { value => $fail_code }); + } + + return $user; +} + +1; + +__END__ + +=head1 NAME + +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 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. + +The hashref may also contain a C<failure_count> element, which specifies +how many times the account has failed to log in within the lockout +period (see L</AUTH_LOCKOUT>). This is used to warn the user when +he is getting close to being locked out. + +=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. + +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>. + +=head2 C<AUTH_LOCKOUT> + +The user's account is locked out after having failed to log in too many +times within a certain period of time (as specified by +L<Bugzilla::Constants/LOGIN_LOCKOUT_INTERVAL>). + +The hashref will also contain a C<user> element, representing the +L<Bugzilla::User> whose account is locked out. + +=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 + +These are methods that can be called on a C<Bugzilla::Auth> object +itself. + +=head2 Login + +=over 4 + +=item C<login($type)> + +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 + +=head2 Info Methods + +These are methods that give information about the Bugzilla::Auth object. + +=over 4 + +=item C<can_change_password> + +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. + +=item C<can_login> + +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. + +=item C<can_logout> + +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>. + +=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<extern_id_used> + +Description: Whether or not current login system uses extern_id. + +=item C<can_change_email> + +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. + +=item C<login_token> + +Description: If a login token was used instead of a cookie then this + will return the current login token data such as user id + and the token itself. +Params: None +Returns: A hash containing C<login_token> and C<user_id>. + +=back + +=head1 STRUCTURE + +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. + +A C<Bugzilla::Auth> object is essentially a collection of a few other +objects: the "Info Getter," the "Verifier," and the "Persistence +Mechanism." + +They are used inside the C<login> function in the following order: + +=head2 The Info Getter + +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.) + +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. + +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>. + +=head2 The Verifier + +This verifies that the username and password are valid. + +It's possible that some methods of verification don't require a password. + +=head2 The Persistence Mechanism + +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." + +=head2 Other Things We Do + +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.) + +=head2 The C<$login_data> Hash + +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. + +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. + +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. + +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. + +Here are all of the elements that *may* be in C<$login_data>: + +=over 4 + +=item C<user_id> + +A Bugzilla C<user_id> that uniquely identifies a user. + +=item C<username> + +The username that was provided by the user. + +=item C<bz_username> + +The username of this user inside of Bugzilla. Sometimes this differs from +C<username>. + +=item C<password> + +The password provided by the user. + +=item C<realname> + +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 |