aboutsummaryrefslogtreecommitdiff
path: root/Bugzilla/Auth.pm
diff options
context:
space:
mode:
Diffstat (limited to 'Bugzilla/Auth.pm')
-rw-r--r--Bugzilla/Auth.pm543
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