From 78ad8c0d088aa95ec1bd7eadea45ffdba05d907e Mon Sep 17 00:00:00 2001 From: Dylan William Hardison Date: Fri, 15 Sep 2017 16:13:18 -0400 Subject: Bug 1364233 - Add setting to force a group to require MFA and restrict users in that group who have not enabled MFA --- Bugzilla.pm | 40 +++++++++++++++++++++---- Bugzilla/Auth.pm | 2 ++ Bugzilla/Config/Auth.pm | 17 ++++++++++- Bugzilla/DB/Schema.pm | 1 + Bugzilla/Install/DB.pm | 1 + Bugzilla/User.pm | 22 ++++++++++++++ enter_bug.cgi | 8 ++--- skins/standard/global.css | 18 ++++++++++- template/en/default/account/prefs/mfa.html.tmpl | 26 +++++++++++++--- template/en/default/admin/params/auth.html.tmpl | 8 +++++ template/en/default/global/header.html.tmpl | 19 ++++++++++-- userprefs.cgi | 2 +- 12 files changed, 145 insertions(+), 19 deletions(-) diff --git a/Bugzilla.pm b/Bugzilla.pm index 0ffd63e04..2e105e0f5 100644 --- a/Bugzilla.pm +++ b/Bugzilla.pm @@ -383,21 +383,49 @@ sub login { # At this point, we now know if a real person is logged in. # Check if a password reset is required - if ($authenticated_user->password_change_required) { + my $cgi = Bugzilla->cgi; + if ( $authenticated_user->password_change_required ) { + # We cannot show the password reset UI for API calls, so treat those as # a disabled account. - if (i_am_webservice()) { - ThrowUserError("account_disabled", { disabled_reason => $authenticated_user->password_change_reason }); + if ( i_am_webservice() ) { + ThrowUserError( "account_disabled", { disabled_reason => $authenticated_user->password_change_reason } ); } # only allow the reset-password and token pages to handle requests # (tokens handles the 'forgot password' process) # otherwise redirect user to the reset-password page. - if ($ENV{SCRIPT_NAME} !~ m#/(?:reset_password|token)\.cgi$#) { - print Bugzilla->cgi->redirect('reset_password.cgi'); + if ( $ENV{SCRIPT_NAME} !~ m#/(?:reset_password|token)\.cgi$# ) { + print $cgi->redirect('reset_password.cgi'); exit; } } + elsif ( !i_am_webservice() && $authenticated_user->in_mfa_group && !$authenticated_user->mfa ) { + + # decide if the user needs a warning or to be blocked. + my $date = $authenticated_user->mfa_required_date('UTC'); + my $grace_period = Bugzilla->params->{mfa_group_grace_period}; + my $expired = defined $date && $date < DateTime->now; + my $on_mfa_page = $cgi->script_name eq '/userprefs.cgi' && $cgi->param('tab') eq 'mfa'; + + Bugzilla->request_cache->{mfa_warning} = 1; + Bugzilla->request_cache->{mfa_grace_period_expired} = $expired; + Bugzilla->request_cache->{on_mfa_page} = $on_mfa_page; + + if ( $grace_period == 0 || $expired) { + if (!$on_mfa_page) { + print Bugzilla->cgi->redirect("userprefs.cgi?tab=mfa"); + exit; + } + } + else { + my $dbh = Bugzilla->dbh_main; + my $date = $dbh->sql_date_math( 'NOW()', '+', '?', 'DAY' ); + my ($mfa_required_date) = $dbh->selectrow_array( "SELECT $date", undef, $grace_period ); + $authenticated_user->set_mfa_required_date($mfa_required_date); + $authenticated_user->update(); + } + } # We must now check to see if an sudo session is in progress. # For a session to be in progress, the following must be true: @@ -1222,4 +1250,4 @@ information. =back -=back +=back \ No newline at end of file diff --git a/Bugzilla/Auth.pm b/Bugzilla/Auth.pm index 797ec1122..58ac248c5 100644 --- a/Bugzilla/Auth.pm +++ b/Bugzilla/Auth.pm @@ -111,6 +111,8 @@ sub login { }); } + + return $self->_handle_login_result($login_info, $type); } diff --git a/Bugzilla/Config/Auth.pm b/Bugzilla/Config/Auth.pm index 58a3d3cd7..612fd1f3f 100644 --- a/Bugzilla/Config/Auth.pm +++ b/Bugzilla/Config/Auth.pm @@ -183,6 +183,21 @@ sub get_param_list { type => 't', default => '', }, + + { + name => 'mfa_group', + type => 's', + choices => \&get_all_group_names, + default => '', + checker => \&check_group, + }, + + { + name => 'mfa_group_grace_period', + type => 't', + default => '7', + checker => \&check_numeric, + } ); return @param_list; } @@ -234,4 +249,4 @@ sub _check_passwdqc_random_bits { return ""; } -1; +1; \ No newline at end of file diff --git a/Bugzilla/DB/Schema.pm b/Bugzilla/DB/Schema.pm index 2c8778c27..7448d8878 100644 --- a/Bugzilla/DB/Schema.pm +++ b/Bugzilla/DB/Schema.pm @@ -936,6 +936,7 @@ use constant ABSTRACT_SCHEMA => { password_change_required => { TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE' }, password_change_reason => { TYPE => 'varchar(64)' }, mfa => {TYPE => 'varchar(8)', DEFAULT => "''" }, + mfa_required_date => {TYPE => 'DATETIME'}, ], INDEXES => [ profiles_login_name_idx => {FIELDS => ['login_name'], diff --git a/Bugzilla/Install/DB.pm b/Bugzilla/Install/DB.pm index 539a7cf78..3b1836c26 100644 --- a/Bugzilla/Install/DB.pm +++ b/Bugzilla/Install/DB.pm @@ -746,6 +746,7 @@ sub update_table_definitions { $dbh->bz_add_column('profiles', 'mfa', { TYPE => 'varchar(8)', , DEFAULT => "''" }); + $dbh->bz_add_column('profiles', 'mfa_required_date', { TYPE => 'DATETIME' }); _migrate_group_owners(); $dbh->bz_add_column('groups', 'idle_member_removal', diff --git a/Bugzilla/User.pm b/Bugzilla/User.pm index 2d8256080..68a3b8313 100644 --- a/Bugzilla/User.pm +++ b/Bugzilla/User.pm @@ -80,6 +80,7 @@ sub DB_COLUMNS { 'profiles.password_change_required', 'profiles.password_change_reason', 'profiles.mfa', + 'profiles.mfa_required_date' ), } @@ -112,6 +113,7 @@ sub UPDATE_COLUMNS { password_change_required password_change_reason mfa + mfa_required_date ); push(@cols, 'cryptpassword') if exists $self->{cryptpassword}; return @cols; @@ -502,6 +504,11 @@ sub set_mfa { delete $self->{mfa_provider}; } +sub set_mfa_required_date { + my ($self, $value) = @_; + $self->set('mfa_required_date', $value); +} + sub set_groups { my $self = shift; $self->_set_groups(GROUP_MEMBERSHIP, @_); @@ -670,6 +677,12 @@ sub authorizer { } sub mfa { $_[0]->{mfa} } + +sub mfa_required_date { + my $self = shift; + return $self->{mfa_required_date} ? datetime_from($self->{mfa_required_date}, @_) : undef; +} + sub mfa_provider { my ($self) = @_; my $mfa = $self->{mfa} || return undef; @@ -679,6 +692,15 @@ sub mfa_provider { return $self->{mfa_provider}; } + +sub in_mfa_group { + my $self = shift; + return $self->{in_mfa_group} if exists $self->{in_mfa_group}; + + my $mfa_group = Bugzilla->params->{mfa_group}; + return $self->{in_mfa_group} = ($mfa_group && $self->in_group($mfa_group)); +} + sub name_or_login { my $self = shift; diff --git a/enter_bug.cgi b/enter_bug.cgi index 0fae8158d..33cdf8535 100755 --- a/enter_bug.cgi +++ b/enter_bug.cgi @@ -395,14 +395,14 @@ $vars->{'bug_status'} = \@statuses; # to the first confirmed bug status on the list, if available. my $picked_status = formvalue('bug_status'); -if ($picked_status and grep($_->name eq $picked_status, @statuses)) { +if ( $picked_status and grep( $_->name eq $picked_status, @statuses ) ) { $default{'bug_status'} = formvalue('bug_status'); -} elsif (scalar @statuses == 1) { +} +elsif ( scalar @statuses == 1 ) { $default{'bug_status'} = $statuses[0]->name; } else { - $default{'bug_status'} = ($statuses[0]->name ne 'UNCONFIRMED') - ? $statuses[0]->name : $statuses[1]->name; + $default{'bug_status'} = ( $statuses[0]->name ne 'UNCONFIRMED' ) ? $statuses[0]->name : $statuses[1]->name; } my @groups = $cgi->param('groups'); diff --git a/skins/standard/global.css b/skins/standard/global.css index e6f63a927..f6579efee 100644 --- a/skins/standard/global.css +++ b/skins/standard/global.css @@ -884,9 +884,25 @@ hr { border-top: 2px solid rgb(255, 255, 255); box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); margin: -15px -15px 0 -15px; - color: transparent; } +#mfa-warning { + outline: none; + border-color: #FF5300; + border-width: 1px; + box-shadow: 2px 2px 15px #FF5300; + color: black; + padding: 2px 2px 2px 2px; +} + +body.mfa-warning #mfa-select button { + outline: none; + border-color: #FF5300; + border-width: 1px; + box-shadow: 2px 2px 15px #FF5300; +} + + #header .subheader { text-align: left; padding-left: 10px; diff --git a/template/en/default/account/prefs/mfa.html.tmpl b/template/en/default/account/prefs/mfa.html.tmpl index fc748cdd1..99a4b0f2a 100644 --- a/template/en/default/account/prefs/mfa.html.tmpl +++ b/template/en/default/account/prefs/mfa.html.tmpl @@ -6,6 +6,8 @@ # defined by the Mozilla Public License, v. 2.0. #%] +[% SET MFA_HOWTO = "https://wiki.mozilla.org/BMO/UserGuide/Two-Factor_Authentication" %] + [% IF NOT Bugzilla.feature('mfa') %]

@@ -126,9 +128,25 @@ [% ELSE %] -

- Two-factor authentication is currently disabled. -

+ [% IF Bugzilla.request_cache.mfa_warning %] +

+ You must enable two-factor authentication + [% UNLESS Bugzilla.request_cache.mfa_grace_period_expired %] + before [% Bugzilla.user.mfa_required_date FILTER time %]. + After that date, you will be restricted to this page until 2FA is configured. + [% ELSE %] + before continuing to use [% terms.Bugzilla %]. + [% END %] +

+

+ Need help setting ip 2FA? + You may want to read these comprensive instructions. +

+ [% ELSE %] +

+ Two-factor authentication is currently disabled. +

+ [% END %] @@ -257,4 +275,4 @@
  • If in doubt, generate and print new recovery codes
  • Do not store these codes electronically
  • -[% END %] +[% END %] \ No newline at end of file diff --git a/template/en/default/admin/params/auth.html.tmpl b/template/en/default/admin/params/auth.html.tmpl index 99c52f759..e19712351 100644 --- a/template/en/default/admin/params/auth.html.tmpl +++ b/template/en/default/admin/params/auth.html.tmpl @@ -244,5 +244,13 @@ "The 'secret key' for Duo 2FA. This value is provided by your " _ "Duo Security administrator.", + mfa_group => + "Members of this group must enable MFA. If the grace period is set, " _ + "users will receive a warning on every page until end of the grace period. " _ + "Users without MFA after the grace period (or when it is set to 0) will only " _ + "be able to access the mfa tab of the user preferences page." + + mfa_group_grace_period => + "Number of days to warn user to turn on 2FA." }, %] diff --git a/template/en/default/global/header.html.tmpl b/template/en/default/global/header.html.tmpl index e808df9bd..1ea652c10 100644 --- a/template/en/default/global/header.html.tmpl +++ b/template/en/default/global/header.html.tmpl @@ -39,7 +39,7 @@ # no_body: if true the body element will not be generated # allow_mobile: allow special CSS and viewport for detected mobile useragents # use_login_page: display a link to the full login page, rather than an inline login. - # no_index: Disable search engine from adding page into search index. + # no_index: Disable search engine from adding page into search index. #%] [% IF message %] @@ -234,6 +234,9 @@ @@ -252,6 +255,18 @@ [% Hook.process("message") %] + [% IF Bugzilla.request_cache.mfa_warning + AND user.mfa_required_date + AND NOT Bugzilla.request_cache.on_mfa_page %] + + Please enabled two-factor authentication + [% IF Param('mfa_group_grace_period') %] + before [% user.mfa_required_date FILTER time %]. + [% ELSE %] + now. + [% END %] + + [% END %] [% IF user.id %] @@ -355,4 +370,4 @@ [% BLOCK format_js_link %] -[% END %] +[% END %] \ No newline at end of file diff --git a/userprefs.cgi b/userprefs.cgi index 7d6a66c6d..00771ceac 100755 --- a/userprefs.cgi +++ b/userprefs.cgi @@ -696,7 +696,7 @@ sub SaveMFAupdate { $user->set_mfa($mfa); $user->mfa_provider->enrolled(); - + Bugzilla->request_cache->{mfa_warning} = 0; my $settings = Bugzilla->user->settings; $settings->{api_key_only}->set('on'); clear_settings_cache(Bugzilla->user->id); -- cgit v1.2.3-24-g4f1b