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\d+)/(?:(?P\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 @@ +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 @@ +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 @@ +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().')',