]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 1364233 - Add setting to force a group to require MFA and restrict users in that...
authorDylan William Hardison <dylan@hardison.net>
Fri, 15 Sep 2017 20:13:18 +0000 (16:13 -0400)
committerGitHub <noreply@github.com>
Fri, 15 Sep 2017 20:13:18 +0000 (16:13 -0400)
12 files changed:
Bugzilla.pm
Bugzilla/Auth.pm
Bugzilla/Config/Auth.pm
Bugzilla/DB/Schema.pm
Bugzilla/Install/DB.pm
Bugzilla/User.pm
enter_bug.cgi
skins/standard/global.css
template/en/default/account/prefs/mfa.html.tmpl
template/en/default/admin/params/auth.html.tmpl
template/en/default/global/header.html.tmpl
userprefs.cgi

index 0ffd63e045bb4dc39cadf33101b12fc83b858d20..2e105e0f5ad206ed68669c3c5a300e1c77f4ba44 100644 (file)
@@ -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
index 797ec1122b5da34f1e92084265b668fa6d6ff440..58ac248c596a0ba4ca2f0c811f05eff41253e67f 100644 (file)
@@ -111,6 +111,8 @@ sub login {
         });
     }
 
+
+
     return $self->_handle_login_result($login_info, $type);
 }
 
index 58a3d3cd7b4578eb99b94817eb6f28b0bb5cf813..612fd1f3fa609518f51cbe0bad456e3148770dbd 100644 (file)
@@ -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
index 2c8778c27c7b72ce0aab1dedb9bd63853ca9161f..7448d887801ebde9e8279646e8aa5ed92e2e2064 100644 (file)
@@ -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'],
index 539a7cf783bfea8e713f454ff7c587df428dd330..3b1836c268c094a2b46810b72cc10af652360ece 100644 (file)
@@ -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',
index 2d82560809b88a33ebe7af1713bca6cc074b407a..68a3b83130f94d9f837dc85d082fa2e5bef8f8e8 100644 (file)
@@ -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;
 
index 0fae8158d07d6983a0a55bea27f3e59568549178..33cdf853520ea62ede594ec8dd7d846ac6e4867f 100755 (executable)
@@ -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');
index e6f63a927b23b284438fac5a84fcaf90c5629007..f6579efee4fe5b5f7beb3b844cd2cdabec92318c 100644 (file)
@@ -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;
index fc748cdd16515d7d4f573d291fc96f81aa12a7f0..99a4b0f2aac227b92d41fe73d3499d2a347be38c 100644 (file)
@@ -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') %]
   <input type="hidden" name="mfa_action" id="mfa-action" value="">
   <p>
     </div>
 
   [% ELSE %]
-    <p>
-      Two-factor authentication is currently <b>disabled</b>.
-    </p>
+    [% IF Bugzilla.request_cache.mfa_warning %]
+      <p class="mfa-warning-msg">
+        You <b>must</b> enable two-factor authentication
+        [% UNLESS Bugzilla.request_cache.mfa_grace_period_expired %]
+          before <i>[% Bugzilla.user.mfa_required_date FILTER time %]</i>.
+          After that date, you will be restricted to this page until 2FA is configured.
+        [% ELSE %]
+          before continuing to use [% terms.Bugzilla %].
+        [% END %]
+      </p>
+      <p>
+        <b>Need help setting ip 2FA?</b>
+        You may want to <a href="[% MFA_HOWTO FILTER html %]">read these comprensive instructions</a>.
+      </p>
+    [% ELSE %]
+      <p>
+        Two-factor authentication is currently <b>disabled</b>.
+      </p>
+    [% END %]
     <input type="hidden" name="mfa_action" id="mfa-action" value="enable">
     <input type="hidden" name="mfa" id="mfa">
 
     <li>If in doubt, generate and print new recovery codes</li>
     <li><b>Do not store these codes electronically</b></li>
   </ul>
-[% END %]
+[% END %]
\ No newline at end of file
index 99c52f759ad2e279996cbe8525d8cb4557cebb9a..e197123519536922e331de2ea5c4658d60fe6c3a 100644 (file)
     "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."
   },
 %]
index e808df9bde660a04c02620f2a3671a6d38706fc6..1ea652c10ac06c868cda49594f72d671b3de2218 100644 (file)
@@ -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 %]
   <body
         class="[% urlbase.replace('^https?://','').replace('/$','').replace('[-~@:/.]+','-') FILTER css_class_quote %]
                skin-[% user.settings.skin.value FILTER css_class_quote %]
+               [% IF Bugzilla.request_cache.mfa_warning %]
+                mfa-warning
+               [% END %]
                [% FOREACH class = bodyclasses %]
                  [% ' ' %][% class FILTER css_class_quote %]
                [% END %] yui-skin-sam">
       </td>
       <td>
         [% Hook.process("message") %]
+        [% IF Bugzilla.request_cache.mfa_warning
+              AND user.mfa_required_date
+              AND NOT Bugzilla.request_cache.on_mfa_page %]
+          <span id="mfa-warning">
+            Please <a href="userprefs.cgi?tab=mfa">enabled two-factor authentication</a>
+            [% IF Param('mfa_group_grace_period') %]
+              before <i>[% user.mfa_required_date FILTER time %]</i>.
+            [% ELSE %]
+              now.
+            [% END %]
+          </span>
+        [% END %]
       </td>
       <td id="moz_login">
         [% IF user.id %]
 
 [% BLOCK format_js_link %]
   <script [% script_nonce FILTER none %] type="text/javascript" src="[% asset_url FILTER mtime FILTER html %]"></script>
-[% END %]
+[% END %]
\ No newline at end of file
index 7d6a66c6d9f641a07bfa254859a8a42482dd4dc2..00771ceac32229ecbffcf3a8aa60fec3d34171ea 100755 (executable)
@@ -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);