From deb80b7652f7ca87c71463b9481af035148f1573 Mon Sep 17 00:00:00 2001
From: epriestley <git@epriestley.com>
Date: Tue, 17 May 2011 18:42:21 -0700
Subject: [PATCH] Provide an activity log for login and administrative actions

Summary: This isn't complete, but I figured I'd ship it for review while it's still smallish.

Provide an activity log for high-level system actions (logins, admin actions). This basically allows two things to happen:

  - The log itself is useful if there are shenanigans.
  - Password login can check it and start CAPTCHA'ing users after a few failed attempts.

I'm going to change how the admin stuff works a little bit too, since right now you can make someone an agent, grab their certificate, revert them back to a normal user, and then act on their behalf over Conduit. This is a little silly, I'm going to move "agent" to the create workflow instead. I'll also add a confirm/email step to the administrative password reset flow.

Test Plan: Took various administrative and non-administrative actions, they appeared in the logs. Filtered the logs in a bunch of different ways.

Reviewers: jungejason, tuomaspelkonen, aran

CC:

Differential Revision: 302
---
 resources/sql/patches/039.userlog.sql         |  27 ++
 src/__phutil_library_map__.php                |   4 +
 ...AphrontDefaultApplicationConfiguration.php |   1 +
 .../login/PhabricatorLoginController.php      |   6 +
 .../auth/controller/login/__init__.php        |   1 +
 .../logout/PhabricatorLogoutController.php    |   8 +
 .../auth/controller/logout/__init__.php       |   1 +
 .../base/PhabricatorPeopleController.php      |  21 ++
 .../edit/PhabricatorPeopleEditController.php  |  56 +++-
 .../people/controller/edit/__init__.php       |   1 +
 .../list/PhabricatorPeopleListController.php  |   2 +-
 .../logs/PhabricatorPeopleLogsController.php  | 244 ++++++++++++++++++
 .../people/controller/logs/__init__.php       |  27 ++
 .../people/storage/log/PhabricatorUserLog.php |  95 +++++++
 .../people/storage/log/__init__.php           |  14 +
 .../people/storage/user/PhabricatorUser.php   |  12 +
 .../people/storage/user/__init__.php          |   1 +
 ...torTypeaheadCommonDatasourceController.php |  15 +-
 18 files changed, 530 insertions(+), 6 deletions(-)
 create mode 100644 resources/sql/patches/039.userlog.sql
 create mode 100644 src/applications/people/controller/logs/PhabricatorPeopleLogsController.php
 create mode 100644 src/applications/people/controller/logs/__init__.php
 create mode 100644 src/applications/people/storage/log/PhabricatorUserLog.php
 create mode 100644 src/applications/people/storage/log/__init__.php

diff --git a/resources/sql/patches/039.userlog.sql b/resources/sql/patches/039.userlog.sql
new file mode 100644
index 0000000000..cb5675ae80
--- /dev/null
+++ b/resources/sql/patches/039.userlog.sql
@@ -0,0 +1,27 @@
+CREATE TABLE phabricator_user.user_log (
+  id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+  actorPHID varchar(64) BINARY,
+  key(actorPHID, dateCreated),
+  userPHID varchar(64) BINARY NOT NULL,
+  key(userPHID, dateCreated),
+  action varchar(64) NOT NULL,
+  key(action, dateCreated),
+  oldValue LONGBLOB NOT NULL,
+  newValue LONGBLOB NOT NULL,
+  details LONGBLOB NOT NULL,
+  dateCreated INT UNSIGNED NOT NULL,
+  dateModified INT UNSIGNED NOT NULL,
+  key(dateCreated)
+);
+
+ALTER TABLE phabricator_user.user_log
+  ADD remoteAddr varchar(16) NOT NULL;
+
+ALTER TABLE phabricator_user.user_log
+  ADD KEY (remoteAddr, dateCreated);
+
+ALTER TABLE phabricator_user.user_log
+  ADD session varchar(40);
+
+ALTER TABLE phabricator_user.user_log
+  ADD KEY (session, dateCreated);
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
index 3fd72b4d28..7ad09c5b74 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -383,6 +383,7 @@ phutil_register_library_map(array(
     'PhabricatorPeopleController' => 'applications/people/controller/base',
     'PhabricatorPeopleEditController' => 'applications/people/controller/edit',
     'PhabricatorPeopleListController' => 'applications/people/controller/list',
+    'PhabricatorPeopleLogsController' => 'applications/people/controller/logs',
     'PhabricatorPeopleProfileController' => 'applications/people/controller/profile',
     'PhabricatorPeopleProfileEditController' => 'applications/people/controller/profileedit',
     'PhabricatorPreferencesController' => 'applications/preferences/controller/base',
@@ -469,6 +470,7 @@ phutil_register_library_map(array(
     'PhabricatorUIPagerExample' => 'applications/uiexample/examples/pager',
     'PhabricatorUser' => 'applications/people/storage/user',
     'PhabricatorUserDAO' => 'applications/people/storage/base',
+    'PhabricatorUserLog' => 'applications/people/storage/log',
     'PhabricatorUserOAuthInfo' => 'applications/people/storage/useroauthinfo',
     'PhabricatorUserPreferences' => 'applications/people/storage/preferences',
     'PhabricatorUserProfile' => 'applications/people/storage/profile',
@@ -805,6 +807,7 @@ phutil_register_library_map(array(
     'PhabricatorPeopleController' => 'PhabricatorController',
     'PhabricatorPeopleEditController' => 'PhabricatorPeopleController',
     'PhabricatorPeopleListController' => 'PhabricatorPeopleController',
+    'PhabricatorPeopleLogsController' => 'PhabricatorPeopleController',
     'PhabricatorPeopleProfileController' => 'PhabricatorPeopleController',
     'PhabricatorPeopleProfileEditController' => 'PhabricatorPeopleController',
     'PhabricatorPreferencesController' => 'PhabricatorController',
@@ -880,6 +883,7 @@ phutil_register_library_map(array(
     'PhabricatorUIPagerExample' => 'PhabricatorUIExample',
     'PhabricatorUser' => 'PhabricatorUserDAO',
     'PhabricatorUserDAO' => 'PhabricatorLiskDAO',
+    'PhabricatorUserLog' => 'PhabricatorUserDAO',
     'PhabricatorUserOAuthInfo' => 'PhabricatorUserDAO',
     'PhabricatorUserPreferences' => 'PhabricatorUserDAO',
     'PhabricatorUserProfile' => 'PhabricatorUserDAO',
diff --git a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php
index d01164cfa1..fe9a116f8e 100644
--- a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php
+++ b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php
@@ -69,6 +69,7 @@ class AphrontDefaultApplicationConfiguration
       ),
       '/people/' => array(
         '$' => 'PhabricatorPeopleListController',
+        'logs/$' => 'PhabricatorPeopleLogsController',
         'edit/(?:(?P<id>\d+)/(?:(?P<view>\w+)/)?)?$'
           => 'PhabricatorPeopleEditController',
       ),
diff --git a/src/applications/auth/controller/login/PhabricatorLoginController.php b/src/applications/auth/controller/login/PhabricatorLoginController.php
index c9cc43d098..1e22147785 100644
--- a/src/applications/auth/controller/login/PhabricatorLoginController.php
+++ b/src/applications/auth/controller/login/PhabricatorLoginController.php
@@ -61,6 +61,12 @@ class PhabricatorLoginController extends PhabricatorAuthController {
 
             return id(new AphrontRedirectResponse())
               ->setURI('/');
+          } else {
+            $log = PhabricatorUserLog::newLog(
+              null,
+              $user,
+              PhabricatorUserLog::ACTION_LOGIN_FAILURE);
+            $log->save();
           }
         }
 
diff --git a/src/applications/auth/controller/login/__init__.php b/src/applications/auth/controller/login/__init__.php
index 4a83ccc586..c0d42fc85c 100644
--- a/src/applications/auth/controller/login/__init__.php
+++ b/src/applications/auth/controller/login/__init__.php
@@ -9,6 +9,7 @@
 phutil_require_module('phabricator', 'aphront/response/redirect');
 phutil_require_module('phabricator', 'applications/auth/controller/base');
 phutil_require_module('phabricator', 'applications/auth/oauth/provider/base');
+phutil_require_module('phabricator', 'applications/people/storage/log');
 phutil_require_module('phabricator', 'applications/people/storage/user');
 phutil_require_module('phabricator', 'infrastructure/env');
 phutil_require_module('phabricator', 'view/form/base');
diff --git a/src/applications/auth/controller/logout/PhabricatorLogoutController.php b/src/applications/auth/controller/logout/PhabricatorLogoutController.php
index 286881b00a..ddf31ad76b 100644
--- a/src/applications/auth/controller/logout/PhabricatorLogoutController.php
+++ b/src/applications/auth/controller/logout/PhabricatorLogoutController.php
@@ -29,8 +29,16 @@ class PhabricatorLogoutController extends PhabricatorAuthController {
 
   public function processRequest() {
     $request = $this->getRequest();
+    $user = $request->getUser();
 
     if ($request->isFormPost()) {
+
+      $log = PhabricatorUserLog::newLog(
+        $user,
+        $user,
+        PhabricatorUserLog::ACTION_LOGOUT);
+      $log->save();
+
       $request->clearCookie('phsid');
       return id(new AphrontRedirectResponse())
         ->setURI('/login/');
diff --git a/src/applications/auth/controller/logout/__init__.php b/src/applications/auth/controller/logout/__init__.php
index 85a14076dc..35da8be9dc 100644
--- a/src/applications/auth/controller/logout/__init__.php
+++ b/src/applications/auth/controller/logout/__init__.php
@@ -8,6 +8,7 @@
 
 phutil_require_module('phabricator', 'aphront/response/redirect');
 phutil_require_module('phabricator', 'applications/auth/controller/base');
+phutil_require_module('phabricator', 'applications/people/storage/log');
 
 phutil_require_module('phutil', 'utils');
 
diff --git a/src/applications/people/controller/base/PhabricatorPeopleController.php b/src/applications/people/controller/base/PhabricatorPeopleController.php
index 62ad31de02..40b7b0d90a 100644
--- a/src/applications/people/controller/base/PhabricatorPeopleController.php
+++ b/src/applications/people/controller/base/PhabricatorPeopleController.php
@@ -24,6 +24,27 @@ abstract class PhabricatorPeopleController extends PhabricatorController {
     $page->setApplicationName('People');
     $page->setBaseURI('/people/');
     $page->setTitle(idx($data, 'title'));
+
+    $tabs = array(
+      'directory' => array(
+        'name' => 'User Directory',
+        'href' => '/people/',
+      ),
+    );
+
+    if ($this->getRequest()->getUser()->getIsAdmin()) {
+      $tabs = array_merge(
+        $tabs,
+        array(
+          'logs' => array(
+            'name' => 'Activity Logs',
+            'href' => '/people/logs/',
+          ),
+        ));
+    }
+
+    $page->setTabs($tabs, idx($data, 'tab'));
+
     $page->setGlyph("\xE2\x99\x9F");
     $page->appendChild($view);
 
diff --git a/src/applications/people/controller/edit/PhabricatorPeopleEditController.php b/src/applications/people/controller/edit/PhabricatorPeopleEditController.php
index fba33053cb..f2b2b68f01 100644
--- a/src/applications/people/controller/edit/PhabricatorPeopleEditController.php
+++ b/src/applications/people/controller/edit/PhabricatorPeopleEditController.php
@@ -159,6 +159,13 @@ class PhabricatorPeopleEditController extends PhabricatorPeopleController {
       if (!$errors) {
         try {
           $user->save();
+
+          $log = PhabricatorUserLog::newLog(
+            $admin,
+            $user,
+            PhabricatorUserLog::ACTION_CREATE);
+          $log->save();
+
           $response = id(new AphrontRedirectResponse())
             ->setURI('/people/edit/'.$user->getID().'/?saved=true');
           return $response;
@@ -259,6 +266,13 @@ class PhabricatorPeopleEditController extends PhabricatorPeopleController {
 
       if (!$errors) {
         $user->save();
+
+        $log = PhabricatorUserLog::newLog(
+          $admin,
+          $user,
+          PhabricatorUserLog::ACTION_RESET_PASSWORD);
+        $log->save();
+
         return id(new AphrontRedirectResponse())
           ->setURI($request->getRequestURI()->alter('saved', 'true'));
       }
@@ -306,16 +320,52 @@ class PhabricatorPeopleEditController extends PhabricatorPeopleController {
     $errors = array();
 
     if ($request->isFormPost()) {
+
+      $log_template = PhabricatorUserLog::newLog(
+        $admin,
+        $user,
+        null);
+
+      $logs = array();
+
       if ($is_self) {
         $errors[] = "You can not edit your own role.";
       } else {
-        $user->setIsAdmin($request->getInt('is_admin'));
-        $user->setIsDisabled($request->getInt('is_disabled'));
-        $user->setIsSystemAgent($request->getInt('is_agent'));
+        $new_admin = (bool)$request->getBool('is_admin');
+        $old_admin = (bool)$user->getIsAdmin();
+        if ($new_admin != $old_admin) {
+          $log = clone $log_template;
+          $log->setAction(PhabricatorUserLog::ACTION_ADMIN);
+          $log->setOldValue($old_admin);
+          $log->setNewValue($new_admin);
+          $user->setIsAdmin($new_admin);
+          $logs[] = $log;
+        }
+
+        $new_disabled = (bool)$request->getBool('is_disabled');
+        $old_disabled = (bool)$user->getIsDisabled();
+        if ($new_disabled != $old_disabled) {
+          $log = clone $log_template;
+          $log->setAction(PhabricatorUserLog::ACTION_DISABLE);
+          $log->setOldValue($old_disabled);
+          $log->setNewValue($new_disabled);
+          $user->setIsDisabled($new_disabled);
+          $logs[] = $log;
+        }
+
+        $new_agent = (bool)$request->getBool('is_agent');
+        $old_agent = (bool)$user->getIsSystemAgent();
+        if ($new_agent != $old_agent) {
+          // TODO: Get rid of this, move it to the create flow.
+          $user->setIsSystemAgent($new_agent);
+        }
       }
 
       if (!$errors) {
         $user->save();
+        foreach ($logs as $log) {
+          $log->save();
+        }
         return id(new AphrontRedirectResponse())
           ->setURI($request->getRequestURI()->alter('saved', 'true'));
       }
diff --git a/src/applications/people/controller/edit/__init__.php b/src/applications/people/controller/edit/__init__.php
index 742bbef230..5c631230d4 100644
--- a/src/applications/people/controller/edit/__init__.php
+++ b/src/applications/people/controller/edit/__init__.php
@@ -9,6 +9,7 @@
 phutil_require_module('phabricator', 'aphront/response/404');
 phutil_require_module('phabricator', 'aphront/response/redirect');
 phutil_require_module('phabricator', 'applications/people/controller/base');
+phutil_require_module('phabricator', 'applications/people/storage/log');
 phutil_require_module('phabricator', 'applications/people/storage/user');
 phutil_require_module('phabricator', 'view/form/base');
 phutil_require_module('phabricator', 'view/form/control/checkbox');
diff --git a/src/applications/people/controller/list/PhabricatorPeopleListController.php b/src/applications/people/controller/list/PhabricatorPeopleListController.php
index 4e4e5069b9..e3e69e8150 100644
--- a/src/applications/people/controller/list/PhabricatorPeopleListController.php
+++ b/src/applications/people/controller/list/PhabricatorPeopleListController.php
@@ -120,7 +120,7 @@ class PhabricatorPeopleListController extends PhabricatorPeopleController {
 
     return $this->buildStandardPageResponse($panel, array(
       'title' => 'People',
-      'tab'   => 'people',
+      'tab'   => 'directory',
       ));
   }
 }
diff --git a/src/applications/people/controller/logs/PhabricatorPeopleLogsController.php b/src/applications/people/controller/logs/PhabricatorPeopleLogsController.php
new file mode 100644
index 0000000000..33810fc3b9
--- /dev/null
+++ b/src/applications/people/controller/logs/PhabricatorPeopleLogsController.php
@@ -0,0 +1,244 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+class PhabricatorPeopleLogsController extends PhabricatorPeopleController {
+
+  public function shouldRequireAdmin() {
+    return true;
+  }
+
+  public function processRequest() {
+    $request = $this->getRequest();
+    $user = $request->getUser();
+
+    $filter_activity = $request->getStr('activity');
+    $filter_ip = $request->getStr('ip');
+    $filter_session = $request->getStr('session');
+
+    $filter_user = $request->getArr('user', array());
+    $filter_actor = $request->getArr('actor', array());
+
+    $user_value = array();
+    $actor_value = array();
+
+    $phids = array_merge($filter_user, $filter_actor);
+    if ($phids) {
+      $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
+      if ($filter_user) {
+        $filter_user = reset($filter_user);
+        $user_value = array(
+          $filter_user => $handles[$filter_user]->getFullName(),
+        );
+      }
+
+      if ($filter_actor) {
+        $filter_actor = reset($filter_actor);
+        $actor_value = array(
+          $filter_actor => $handles[$filter_actor]->getFullName(),
+        );
+      }
+    }
+
+    $form = new AphrontFormView();
+    $form
+      ->setUser($user)
+      ->appendChild(
+        id(new AphrontFormTokenizerControl())
+          ->setLabel('Filter Actor')
+          ->setName('actor')
+          ->setLimit(1)
+          ->setValue($actor_value)
+          ->setDatasource('/typeahead/common/accounts/'))
+      ->appendChild(
+        id(new AphrontFormTokenizerControl())
+          ->setLabel('Filter User')
+          ->setName('user')
+          ->setLimit(1)
+          ->setValue($user_value)
+          ->setDatasource('/typeahead/common/accounts/'))
+      ->appendChild(
+        id(new AphrontFormSelectControl())
+          ->setLabel('Show Activity')
+          ->setName('activity')
+          ->setValue($filter_activity)
+          ->setOptions(
+            array(
+              ''        => 'All Activity',
+              'admin'   => 'Admin Activity',
+            )))
+      ->appendChild(
+        id(new AphrontFormTextControl())
+          ->setLabel('Filter IP')
+          ->setName('ip')
+          ->setValue($filter_ip)
+          ->setCaption(
+            'Enter an IP (or IP prefix) to show only activity by that remote '.
+            'address.'))
+      ->appendChild(
+        id(new AphrontFormTextControl())
+          ->setLabel('Filter Session')
+          ->setName('session')
+          ->setValue($filter_session))
+      ->appendChild(
+        id(new AphrontFormSubmitControl())
+          ->setValue('Filter Logs'));
+
+    $log_table = new PhabricatorUserLog();
+    $conn_r = $log_table->establishConnection('r');
+
+    $where_clause = array();
+    $where_clause[] = '1 = 1';
+
+    if ($filter_user) {
+      $where_clause[] = qsprintf(
+        $conn_r,
+        'userPHID = %s',
+        $filter_user);
+    }
+
+    if ($filter_actor) {
+      $where_clause[] = qsprintf(
+        $conn_r,
+        'actorPHID = %s',
+        $filter_actor);
+    }
+
+    if ($filter_activity == 'admin') {
+      $where_clause[] = qsprintf(
+        $conn_r,
+        'action NOT IN (%Ls)',
+        array(
+          PhabricatorUserLog::ACTION_LOGIN,
+          PhabricatorUserLog::ACTION_LOGOUT,
+          PhabricatorUserLog::ACTION_LOGIN_FAILURE,
+        ));
+    }
+
+    if ($filter_ip) {
+      $where_clause[] = qsprintf(
+        $conn_r,
+        'remoteAddr LIKE %>',
+        $filter_ip);
+    }
+
+    if ($filter_session) {
+      $where_clause[] = qsprintf(
+        $conn_r,
+        'session = %s',
+        $filter_session);
+    }
+
+    $where_clause = '('.implode(') AND (', $where_clause).')';
+
+    $pager = new AphrontPagerView();
+    $pager->setURI($request->getRequestURI(), 'page');
+    $pager->setOffset($request->getInt('page'));
+    $pager->setPageSize(500);
+
+    $logs = $log_table->loadAllWhere(
+      '(%Q) ORDER BY dateCreated DESC LIMIT %d, %d',
+      $where_clause,
+      $pager->getOffset(),
+      $pager->getPageSize() + 1);
+
+    $logs = $pager->sliceResults($logs);
+
+    $phids = array();
+    foreach ($logs as $log) {
+      $phids[$log->getActorPHID()] = true;
+      $phids[$log->getUserPHID()] = true;
+    }
+    $phids = array_keys($phids);
+    $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
+
+    $rows = array();
+    foreach ($logs as $log) {
+      $rows[] = array(
+        date('M jS, Y', $log->getDateCreated()),
+        date('g:i:s A', $log->getDateCreated()),
+        $log->getAction(),
+        $log->getActorPHID()
+          ? phutil_escape_html($handles[$log->getActorPHID()]->getName())
+          : null,
+        phutil_escape_html($handles[$log->getUserPHID()]->getName()),
+        json_encode($log->getOldValue(), true),
+        json_encode($log->getNewValue(), true),
+        phutil_render_tag(
+          'a',
+          array(
+            'href' => $request
+              ->getRequestURI()
+              ->alter('ip', $log->getRemoteAddr()),
+          ),
+          phutil_escape_html($log->getRemoteAddr())),
+        phutil_render_tag(
+          'a',
+          array(
+            'href' => $request
+              ->getRequestURI()
+              ->alter('session', $log->getSession()),
+          ),
+          phutil_escape_html($log->getSession())),
+      );
+    }
+
+    $table = new AphrontTableView($rows);
+    $table->setHeaders(
+      array(
+        'Date',
+        'Time',
+        'Action',
+        'Actor',
+        'User',
+        'Old',
+        'New',
+        'IP',
+        'Session',
+      ));
+    $table->setColumnClasses(
+      array(
+        '',
+        'right',
+        '',
+        '',
+        '',
+        'wrap',
+        'wrap',
+        '',
+        'wide',
+      ));
+
+    $panel = new AphrontPanelView();
+    $panel->setHeader('Activity Logs');
+    $panel->appendChild($table);
+    $panel->appendChild($pager);
+
+    $filter = new AphrontListFilterView();
+    $filter->appendChild($form);
+
+    return $this->buildStandardPageResponse(
+      array(
+        $filter,
+        $panel,
+      ),
+      array(
+        'title' => 'Activity Logs',
+        'tab'   => 'logs',
+      ));
+  }
+}
diff --git a/src/applications/people/controller/logs/__init__.php b/src/applications/people/controller/logs/__init__.php
new file mode 100644
index 0000000000..824368dac3
--- /dev/null
+++ b/src/applications/people/controller/logs/__init__.php
@@ -0,0 +1,27 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('phabricator', 'applications/people/controller/base');
+phutil_require_module('phabricator', 'applications/people/storage/log');
+phutil_require_module('phabricator', 'applications/phid/handle/data');
+phutil_require_module('phabricator', 'storage/qsprintf');
+phutil_require_module('phabricator', 'view/control/pager');
+phutil_require_module('phabricator', 'view/control/table');
+phutil_require_module('phabricator', 'view/form/base');
+phutil_require_module('phabricator', 'view/form/control/select');
+phutil_require_module('phabricator', 'view/form/control/submit');
+phutil_require_module('phabricator', 'view/form/control/text');
+phutil_require_module('phabricator', 'view/form/control/tokenizer');
+phutil_require_module('phabricator', 'view/layout/listfilter');
+phutil_require_module('phabricator', 'view/layout/panel');
+
+phutil_require_module('phutil', 'markup');
+phutil_require_module('phutil', 'utils');
+
+
+phutil_require_source('PhabricatorPeopleLogsController.php');
diff --git a/src/applications/people/storage/log/PhabricatorUserLog.php b/src/applications/people/storage/log/PhabricatorUserLog.php
new file mode 100644
index 0000000000..41fad517c3
--- /dev/null
+++ b/src/applications/people/storage/log/PhabricatorUserLog.php
@@ -0,0 +1,95 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+class PhabricatorUserLog extends PhabricatorUserDAO {
+
+  const ACTION_LOGIN          = 'login';
+  const ACTION_LOGOUT         = 'logout';
+  const ACTION_LOGIN_FAILURE  = 'login-fail';
+  const ACTION_RESET_PASSWORD = 'reset-pass';
+
+  const ACTION_CREATE         = 'create';
+
+  const ACTION_ADMIN          = 'admin';
+  const ACTION_DISABLE        = 'disable';
+
+  protected $actorPHID;
+  protected $userPHID;
+  protected $action;
+  protected $oldValue;
+  protected $newValue;
+  protected $details = array();
+  protected $remoteAddr;
+  protected $session;
+
+  public static function newLog(
+    PhabricatorUser $actor = null,
+    PhabricatorUser $user = null,
+    $action) {
+
+    $log = new PhabricatorUserLog();
+
+    if ($actor) {
+      $log->setActorPHID($actor->getPHID());
+    }
+
+    if ($user) {
+      $log->setUserPHID($user->getPHID());
+    }
+
+    if ($action) {
+      $log->setAction($action);
+    }
+
+    return $log;
+  }
+
+  public function save() {
+    if (!$this->remoteAddr) {
+      $this->remoteAddr = idx($_SERVER, 'REMOTE_ADDR');
+    }
+    if (!$this->session) {
+      $this->setSession(idx($_COOKIE, 'phsid'));
+    }
+    $this->details['host'] = php_uname('n');
+    $this->details['user_agent'] = idx($_SERVER, 'HTTP_USER_AGENT');
+
+    return parent::save();
+  }
+
+  public function setSession($session) {
+    // Store the hash of the session, not the actual session key, so that
+    // seeing the logs doesn't compromise all the sessions which appear in
+    // them. This just prevents casual leaks, like in a screenshot.
+    if (strlen($session)) {
+      $this->session = sha1($session);
+    }
+    return $this;
+  }
+
+  public function getConfiguration() {
+    return array(
+      self::CONFIG_SERIALIZATION => array(
+        'oldValue' => self::SERIALIZATION_JSON,
+        'newValue' => self::SERIALIZATION_JSON,
+        'details'  => self::SERIALIZATION_JSON,
+      ),
+    ) + parent::getConfiguration();
+  }
+
+}
diff --git a/src/applications/people/storage/log/__init__.php b/src/applications/people/storage/log/__init__.php
new file mode 100644
index 0000000000..90f82b5ed8
--- /dev/null
+++ b/src/applications/people/storage/log/__init__.php
@@ -0,0 +1,14 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('phabricator', 'applications/people/storage/base');
+
+phutil_require_module('phutil', 'utils');
+
+
+phutil_require_source('PhabricatorUserLog.php');
diff --git a/src/applications/people/storage/user/PhabricatorUser.php b/src/applications/people/storage/user/PhabricatorUser.php
index 8fcb289b5c..d4fc5f50cb 100644
--- a/src/applications/people/storage/user/PhabricatorUser.php
+++ b/src/applications/people/storage/user/PhabricatorUser.php
@@ -227,6 +227,18 @@ class PhabricatorUser extends PhabricatorUserDAO {
       $establish_type,
       $session_key);
 
+    $log = PhabricatorUserLog::newLog(
+      $this,
+      $this,
+      PhabricatorUserLog::ACTION_LOGIN);
+    $log->setDetails(
+      array(
+        'session_type' => $session_type,
+        'session_issued' => $establish_type,
+      ));
+    $log->setSession($session_key);
+    $log->save();
+
     return $session_key;
   }
 
diff --git a/src/applications/people/storage/user/__init__.php b/src/applications/people/storage/user/__init__.php
index 8a7b435549..e3a2672545 100644
--- a/src/applications/people/storage/user/__init__.php
+++ b/src/applications/people/storage/user/__init__.php
@@ -7,6 +7,7 @@
 
 
 phutil_require_module('phabricator', 'applications/people/storage/base');
+phutil_require_module('phabricator', 'applications/people/storage/log');
 phutil_require_module('phabricator', 'applications/people/storage/preferences');
 phutil_require_module('phabricator', 'applications/phid/constants');
 phutil_require_module('phabricator', 'applications/phid/storage/phid');
diff --git a/src/applications/typeahead/controller/common/PhabricatorTypeaheadCommonDatasourceController.php b/src/applications/typeahead/controller/common/PhabricatorTypeaheadCommonDatasourceController.php
index 068690600e..c3038485af 100644
--- a/src/applications/typeahead/controller/common/PhabricatorTypeaheadCommonDatasourceController.php
+++ b/src/applications/typeahead/controller/common/PhabricatorTypeaheadCommonDatasourceController.php
@@ -26,6 +26,7 @@ class PhabricatorTypeaheadCommonDatasourceController
   public function processRequest() {
 
     $need_users = false;
+    $need_all_users = false;
     $need_lists = false;
     $need_projs = false;
     $need_repos = false;
@@ -35,6 +36,7 @@ class PhabricatorTypeaheadCommonDatasourceController
       case 'searchowner':
         $need_users = true;
         $need_upforgrabs = true;
+        break;
       case 'users':
         $need_users = true;
         break;
@@ -51,6 +53,10 @@ class PhabricatorTypeaheadCommonDatasourceController
       case 'packages':
         $need_packages = true;
         break;
+      case 'accounts':
+        $need_users = true;
+        $need_all_users = true;
+        break;
     }
 
     $data = array();
@@ -66,8 +72,13 @@ class PhabricatorTypeaheadCommonDatasourceController
     if ($need_users) {
       $users = id(new PhabricatorUser())->loadAll();
       foreach ($users as $user) {
-        if ($user->getIsSystemAgent()) {
-          continue;
+        if (!$need_all_users) {
+          if ($user->getIsSystemAgent()) {
+            continue;
+          }
+          if ($user->getIsDisabled()) {
+            continue;
+          }
         }
         $data[] = array(
           $user->getUsername().' ('.$user->getRealName().')',