ACP: Added lockout management feature
authorDan
Thu, 17 Dec 2009 04:31:55 -0500
changeset 1170 71cb87b7dc3f
parent 1169 d5474f54a525
child 1171 d42d46e13b36
ACP: Added lockout management feature
includes/sessions.php
install/schemas/mysql_stage2.sql
install/schemas/postgresql_stage2.sql
install/schemas/upgrade/1.1.6-1.1.7-mysql.sql
install/schemas/upgrade/1.1.6-1.1.7-postgresql.sql
language/english/admin.json
language/english/core.json
plugins/SpecialAdmin.php
plugins/admin/Home.php
plugins/admin/UserManager.php
themes/admin/css/default.css
--- a/includes/sessions.php	Thu Dec 17 04:29:55 2009 -0500
+++ b/includes/sessions.php	Thu Dec 17 04:31:55 2009 -0500
@@ -737,19 +737,19 @@
           'lockout_policy' => 'disable'
           );
       
-      if ( $lockout_data['lockout_policy'] != 'disable' && !defined('IN_ENANO_INSTALL') )
+      if ( $lockout_data['policy'] != 'disable' && !defined('IN_ENANO_INSTALL') )
       {
         $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']);
         // increment fail count
-        $this->sql('INSERT INTO '.table_prefix.'lockout(ipaddr, timestamp, action) VALUES(\'' . $ipaddr . '\', ' . time() . ', \'credential\');');
-        $lockout_data['lockout_fails']++;
+        $this->sql('INSERT INTO '.table_prefix.'lockout(ipaddr, timestamp, action, username) VALUES(\'' . $ipaddr . '\', ' . time() . ', \'credential\', \'' . $db->escape($username) . '\');');
+        $lockout_data['fails']++;
         return array(
             'success' => false,
-            'error' => ( $lockout_data['lockout_fails'] >= $lockout_data['lockout_threshold'] ) ? 'locked_out' : 'invalid_credentials',
-            'lockout_threshold' => $lockout_data['lockout_threshold'],
-            'lockout_duration' => ( $lockout_data['lockout_duration'] ),
-            'lockout_fails' => $lockout_data['lockout_fails'],
-            'lockout_policy' => $lockout_data['lockout_policy']
+            'error' => ( $lockout_data['fails'] >= $lockout_data['threshold'] ) ? 'locked_out' : 'invalid_credentials',
+            'lockout_threshold' => $lockout_data['threshold'],
+            'lockout_duration' => ( $lockout_data['duration'] ),
+            'lockout_fails' => $lockout_data['fails'],
+            'lockout_policy' => $lockout_data['policy']
           );
       }
       
@@ -866,7 +866,7 @@
       {
         $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']);
         // increment fail count
-        $this->sql('INSERT INTO '.table_prefix.'lockout(ipaddr, timestamp, action) VALUES(\'' . $ipaddr . '\', ' . time() . ', \'credential\');');
+        $this->sql('INSERT INTO '.table_prefix.'lockout(ipaddr, timestamp, action) VALUES(\'' . $ipaddr . '\', ' . time() . ', \'credential\', \'' . $db->escape($username) . '\');');
       }
         
       return array(
@@ -981,7 +981,7 @@
     if(!is_int($user_id))
       die('Somehow an SQL injection attempt crawled into our session registrar! (1)');
     if(!is_int($level))
-      die('Somehow an SQL injection attempt crawled into our session registrar! (2)');
+      die(var_dump($level) . '<br />Somehow an SQL injection attempt crawled into our session registrar! (2)');
     
     // Update RAM
     $this->user_id = $user_id;
--- a/install/schemas/mysql_stage2.sql	Thu Dec 17 04:29:55 2009 -0500
+++ b/install/schemas/mysql_stage2.sql	Thu Dec 17 04:31:55 2009 -0500
@@ -274,6 +274,7 @@
   ipaddr varchar(40) NOT NULL,
   action ENUM('credential', 'level') NOT NULL DEFAULT 'credential',
   timestamp int(12) NOT NULL DEFAULT 0,
+  username varchar(255) NOT NULL DEFAULT '',
   PRIMARY KEY ( id )
 ) CHARACTER SET `utf8` COLLATE `utf8_bin`;
 
--- a/install/schemas/postgresql_stage2.sql	Thu Dec 17 04:29:55 2009 -0500
+++ b/install/schemas/postgresql_stage2.sql	Thu Dec 17 04:31:55 2009 -0500
@@ -275,6 +275,7 @@
   ipaddr varchar(40) NOT NULL,
   action varchar(20) NOT NULL DEFAULT 'credential',
   timestamp int NOT NULL DEFAULT 0,
+  username varchar(255) NOT NULL DEFAULT '',
   CHECK ( action IN ('credential', 'level') ),
   PRIMARY KEY ( id )
 );
--- a/install/schemas/upgrade/1.1.6-1.1.7-mysql.sql	Thu Dec 17 04:29:55 2009 -0500
+++ b/install/schemas/upgrade/1.1.6-1.1.7-mysql.sql	Thu Dec 17 04:31:55 2009 -0500
@@ -1,2 +1,4 @@
 ALTER TABLE {{TABLE_PREFIX}}users_extra ADD COLUMN date_format varchar(32) NOT NULL DEFAULT 'F d, Y';
 ALTER TABLE {{TABLE_PREFIX}}users_extra ADD COLUMN time_format varchar(32) NOT NULL DEFAULT 'G:i';
+ALTER TABLE {{TABLE_PREFIX}}lockout ADD COLUMN username varchar(255) NOT NULL DEFAULT '';
+
--- a/install/schemas/upgrade/1.1.6-1.1.7-postgresql.sql	Thu Dec 17 04:29:55 2009 -0500
+++ b/install/schemas/upgrade/1.1.6-1.1.7-postgresql.sql	Thu Dec 17 04:31:55 2009 -0500
@@ -1,2 +1,4 @@
 ALTER TABLE {{TABLE_PREFIX}}users_extra ADD COLUMN date_format varchar(32) NOT NULL DEFAULT 'F d, Y';
 ALTER TABLE {{TABLE_PREFIX}}users_extra ADD COLUMN time_format varchar(32) NOT NULL DEFAULT 'G:i';
+ALTER TABLE {{TABLE_PREFIX}}lockout ADD COLUMN username varchar(255) NOT NULL DEFAULT '';
+
--- a/language/english/admin.json	Thu Dec 17 04:29:55 2009 -0500
+++ b/language/english/admin.json	Thu Dec 17 04:31:55 2009 -0500
@@ -235,16 +235,33 @@
       stat_lastupdate_never: 'Never',
       
       heading_alerts: 'Active alerts',
+      
       msg_demo_title: 'Enano is running in demo mode.',
       msg_demo_body: 'If you borked something up, or if you\'re done testing, you can <a href="%reset_url%">reset this site</a>. The site is reset automatically once every two hours. When a reset is performed, all custom modifications to the site are lost and replaced with default values.',
+      
       msg_install_files_title: 'Installer files found',
       msg_install_files_body: 'Please delete the install/ directory from your Enano installation folder &ndash; it contains sensitive tools that might allow your site to be compromised.',
+      
       heading_updates: 'Check for updates',
       msg_updates_info: 'The Enano team will on occasion release new versions of Enano. We always recommend that you run the latest available version because many releases contain security patches. Enano checks for updates by looking at an <a href="%updates_url%">XML file</a> and doesn\'t share any information about your site.',
       btn_check_updates: 'Check for updates',
+      
       heading_inactive_users: 'Users are awaiting activation',
       msg_inactive_users_one: '1 user has requested manual account activation. You can activate the account by going to the <a %um_flags%>User Manager</a>.',
       msg_inactive_users_plural: '%num_users% users have requested manual account activation. You can activate those accounts by going to the <a %um_flags%>User Manager</a>.',
+      
+      msg_users_locked_out: 'Active IP address lockouts',
+      msg_users_locked_out_hint: 'The following IP addresses have been automatically locked out from login attempts. You can delete these active lockouts, if you choose.',
+      th_locked_out_ip: 'IP address',
+      th_locked_out_username: 'Username (most recent attempt)',
+      th_locked_out_status: 'Status',
+      th_locked_out_time: 'Time remaining',
+      lbl_locked_out_warned: 'Warned (failures: %fail_count%)',
+      lbl_locked_out_banned: 'Locked out',
+      btn_lockout_unblock: 'Unblock',
+      btn_lockout_clear: 'Clear',
+      msg_lockout_clear_success: 'The IP address %ip% has been cleared from the active lockout list.',
+      
       heading_docs: 'Enano documentation',
       msg_docs_info: 'The <a href="http://docs.enanocms.org/" onclick="window.open(this.href); return false;">Enano administrator\'s handbook</a> is maintained as a wiki. It will help you get started with Enano and learn about how we do things differently.',
       heading_support: 'Get support',
--- a/language/english/core.json	Thu Dec 17 04:29:55 2009 -0500
+++ b/language/english/core.json	Thu Dec 17 04:31:55 2009 -0500
@@ -801,6 +801,10 @@
       unit_months: 'months',
       unit_year: 'year',
       unit_years: 'years',
+      unit_minute: 'minute',
+      unit_minutes: 'minutes',
+      unit_minute_short: 'min',
+      unit_minutes_short: 'mins'
     }
   }
 };
--- a/plugins/SpecialAdmin.php	Thu Dec 17 04:29:55 2009 -0500
+++ b/plugins/SpecialAdmin.php	Thu Dec 17 04:31:55 2009 -0500
@@ -671,7 +671,7 @@
           <small><?php echo $lang->get('acpgc_field_passminimum_hint'); ?></small>
         </td>
         <td class="row1">
-          <input type="text" name="pw_strength_minimum" value="<?php echo ( $x = getConfig('pw_strength_minimum') ) ? $x : '-10'; ?>" />
+          <input type="text" name="pw_strength_minimum" value="<?php echo strval(getConfig('pw_strength_minimum', -10)); ?>" />
         </td>
       </tr>
       
@@ -2081,7 +2081,7 @@
     echo $lang->get('adm_page_tagline');
     ?>
     <script type="text/javascript">
-    function ajaxPage(t)
+    function ajaxPage(t, qs)
     {
       if ( KILL_SWITCH )
       {
@@ -2134,9 +2134,9 @@
           });
         return;
       }
-      ajaxPageBin(t);
+      ajaxPageBin(t, qs);
     }
-    function ajaxPageBin(t)
+    function ajaxPageBin(t, qs)
     {
       if ( KILL_SWITCH )
       {
@@ -2144,8 +2144,11 @@
         return false;
       }
       document.getElementById('ajaxPageContainer').innerHTML = '<div class="wait-box">Loading page...</div>';
-      ajaxGet('<?php echo scriptPath; ?>/ajax.php?title='+t+'&_mode=getpage&noheaders&auth=' + ENANO_SID, function(ajax) {
-          if ( ajax.readyState == 4 && ajax.status == 200 ) {
+      qs = qs ? '&' + qs : '';
+      ajaxGet(makeUrl(t, 'noheaders' + qs), function(ajax)
+        {
+          if ( ajax.readyState == 4 && ajax.status == 200 )
+          {
             var response = String(ajax.responseText + '');
             if ( check_json_response(response) )
             {
--- a/plugins/admin/Home.php	Thu Dec 17 04:29:55 2009 -0500
+++ b/plugins/admin/Home.php	Thu Dec 17 04:31:55 2009 -0500
@@ -79,6 +79,8 @@
   }
   $db->free_result();
   
+  acp_usermanager_lockouts(true);
+  
   // Update checker
   echo '<div class="acphome-box info">';
     echo '<h3>' . $lang->get('acphome_heading_updates') . '</h3>';
--- a/plugins/admin/UserManager.php	Thu Dec 17 04:29:55 2009 -0500
+++ b/plugins/admin/UserManager.php	Thu Dec 17 04:31:55 2009 -0500
@@ -590,10 +590,12 @@
               </tr>';
       }
       echo '</table>';
+      echo '</div>';
     }
     $db->free_result();
   }
   
+  acp_usermanager_lockouts();
 }
 
 /**
@@ -1221,4 +1223,81 @@
   
 }
 
-?>
+function acp_usermanager_lockouts($homewrap = false)
+{
+  global $db, $session, $paths, $template, $plugins; // Common objects
+  global $lang;
+  
+  // Locked out users
+  
+  if ( !empty($_GET['clear_lockout']) && is_valid_ip($_GET['clear_lockout']) )
+  {
+    $ip = $db->escape($_GET['clear_lockout']);
+    $q = $db->sql_query('DELETE FROM ' . table_prefix . "lockout WHERE ipaddr = '$ip' AND timestamp > ( " . time() . " - (" . getConfig('lockout_duration', 15) . "*60) );");
+    if ( !$q )
+      $db->_die();
+    
+    echo '<div class="info-box">' . $lang->get('acphome_msg_lockout_clear_success', array('ip' => htmlspecialchars($ip))) . '</div>';
+  }
+  
+  $q = $db->sql_query('SELECT COUNT(id) AS fail_count, ipaddr, username, timestamp FROM ' . table_prefix . "lockout\n"
+                    . "  WHERE timestamp > ( " . time() . " - " . intval(getConfig('lockout_duration', 15)) . "*60 ) GROUP BY ipaddr ORDER BY COUNT(id) DESC, timestamp DESC;");
+  if ( !$q )
+    $db->_die();
+  
+  if ( $db->numrows() > 0 )
+  {
+    if ( $homewrap )
+      echo '<div class="acphome-box notice">';
+    echo '<h3>' . $lang->get('acphome_msg_users_locked_out') . '</h3>';
+    echo '<p>' . $lang->get('acphome_msg_users_locked_out_hint') . '</p>';
+    
+    ?>
+    <div class="tblholder" style="margin-bottom: 10px;">
+    <table width="100%" cellspacing="1" cellpadding="4">
+      <tr>
+        <th><?php echo $lang->get('acphome_th_locked_out_ip'); ?></th>
+        <th><?php echo $lang->get('acphome_th_locked_out_username'); ?></th>
+        <th><?php echo $lang->get('acphome_th_locked_out_status'); ?></th>
+        <th><?php echo $lang->get('acphome_th_locked_out_time'); ?></th>
+        <th></th>
+      </tr>
+    <?php
+    
+    while ( $row = $db->fetchrow() )
+    {
+      echo '<tr>';
+      echo '<td class="row1">' . htmlspecialchars($row['ipaddr']) . '</td>';
+      echo '<td class="row2">' . htmlspecialchars($row['username']) . '</td>';
+      // status
+      echo '<td class="row1" style="text-align: center;">' .
+            ( $row['fail_count'] >= getConfig('lockout_threshold', 5)
+                ? '<b>' . $lang->get('acphome_lbl_locked_out_banned') . '</b>'
+                : $lang->get('acphome_lbl_locked_out_warned', array('fail_count' => $row['fail_count']))
+            )
+            . '</td>';
+      // time left
+      if ( $row['fail_count'] >= getConfig('lockout_threshold', 5) )
+      {
+        $expire_time = $row['timestamp'] + ( getConfig('lockout_duration', 15) * 60 );
+        $time_left = round(($expire_time - time()) / 60);
+        $minutes = $time_left == 1 ? $lang->get('etc_unit_minute') : $lang->get('etc_unit_minutes');
+        echo '<td class="row2" style="text-align: center;">' . "$time_left $minutes" . '</td>';
+      }
+      else
+      {
+        echo '<td class="row2" style="text-align: center;">&ndash;</td>';
+      }
+      // action
+      $btn_text = $row['fail_count'] >= getConfig('lockout_threshold', 5) ? $lang->get('acphome_btn_lockout_unblock') : $lang->get('acphome_btn_lockout_clear');
+      echo '<td class="row1" style="text-align: center;"><a href="#" onclick="ajaxPage(\'' . $paths->nslist['Admin'] . 'UserManager\', \'clear_lockout=' . htmlspecialchars($row['ipaddr']) . '\'); return false;">' . $btn_text . '</a></td>';
+      echo '</tr>';
+    }
+    echo '</table>';
+    echo '</div>';
+    if ( $homewrap )
+      echo '</div>';
+  }
+  
+  $db->free_result();
+}
--- a/themes/admin/css/default.css	Thu Dec 17 04:29:55 2009 -0500
+++ b/themes/admin/css/default.css	Thu Dec 17 04:31:55 2009 -0500
@@ -224,6 +224,10 @@
   margin: 0 0 10px 0;
 }
 
+div.acphome-box div.tblholder table {
+  color: black;
+}
+
 div.acphome-box.warning {
   background-color: #900000;
   color: #fff;
@@ -249,6 +253,14 @@
   color: #fff !important;
 }
 
+div.acphome-box div.tblholder table a {
+  color: #294F75 !important;
+}
+
+div.acphome-box div.tblholder table a:hover {
+  color: #597FA5 !important;
+}
+
 th.systemversion a {
   font-weight: normal;
   color: #fff !important;