Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F13956653
D8683.id.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
14 KB
Referenced Files
None
Subscribers
None
D8683.id.diff
View Options
diff --git a/resources/sql/autopatches/20140402.actionlog.sql b/resources/sql/autopatches/20140402.actionlog.sql
new file mode 100644
--- /dev/null
+++ b/resources/sql/autopatches/20140402.actionlog.sql
@@ -0,0 +1,12 @@
+CREATE TABLE {$NAMESPACE}_system.system_actionlog (
+ id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ actorHash CHAR(12) NOT NULL COLLATE latin1_bin,
+ actorIdentity VARCHAR(255) NOT NULL COLLATE utf8_bin,
+ action CHAR(32) NOT NULL COLLATE utf8_bin,
+ score DOUBLE NOT NULL,
+ epoch INT UNSIGNED NOT NULL,
+
+ KEY `key_epoch` (epoch),
+ KEY `key_action` (actorHash, action, epoch)
+
+) ENGINE=InnoDB, COLLATE utf8_general_ci;
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -2043,6 +2043,7 @@
'PhabricatorSearchWorker' => 'applications/search/worker/PhabricatorSearchWorker.php',
'PhabricatorSecurityConfigOptions' => 'applications/config/option/PhabricatorSecurityConfigOptions.php',
'PhabricatorSendGridConfigOptions' => 'applications/config/option/PhabricatorSendGridConfigOptions.php',
+ 'PhabricatorSettingsAddEmailAction' => 'applications/settings/action/PhabricatorSettingsAddEmailAction.php',
'PhabricatorSettingsAdjustController' => 'applications/settings/controller/PhabricatorSettingsAdjustController.php',
'PhabricatorSettingsMainController' => 'applications/settings/controller/PhabricatorSettingsMainController.php',
'PhabricatorSettingsPanel' => 'applications/settings/panel/PhabricatorSettingsPanel.php',
@@ -2142,6 +2143,12 @@
'PhabricatorSymbolNameLinter' => 'infrastructure/lint/hook/PhabricatorSymbolNameLinter.php',
'PhabricatorSyntaxHighlighter' => 'infrastructure/markup/PhabricatorSyntaxHighlighter.php',
'PhabricatorSyntaxHighlightingConfigOptions' => 'applications/config/option/PhabricatorSyntaxHighlightingConfigOptions.php',
+ 'PhabricatorSystemAction' => 'applications/system/action/PhabricatorSystemAction.php',
+ 'PhabricatorSystemActionEngine' => 'applications/system/engine/PhabricatorSystemActionEngine.php',
+ 'PhabricatorSystemActionGarbageCollector' => 'applications/system/garbagecollector/PhabricatorSystemActionGarbageCollector.php',
+ 'PhabricatorSystemActionLog' => 'applications/system/storage/PhabricatorSystemActionLog.php',
+ 'PhabricatorSystemActionRateLimitException' => 'applications/system/exception/PhabricatorSystemActionRateLimitException.php',
+ 'PhabricatorSystemDAO' => 'applications/system/storage/PhabricatorSystemDAO.php',
'PhabricatorTaskmasterDaemon' => 'infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php',
'PhabricatorTestCase' => 'infrastructure/testing/PhabricatorTestCase.php',
'PhabricatorTestController' => 'applications/base/controller/__tests__/PhabricatorTestController.php',
@@ -4906,6 +4913,7 @@
'PhabricatorSearchWorker' => 'PhabricatorWorker',
'PhabricatorSecurityConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorSendGridConfigOptions' => 'PhabricatorApplicationConfigOptions',
+ 'PhabricatorSettingsAddEmailAction' => 'PhabricatorSystemAction',
'PhabricatorSettingsAdjustController' => 'PhabricatorController',
'PhabricatorSettingsMainController' => 'PhabricatorController',
'PhabricatorSettingsPanelAccount' => 'PhabricatorSettingsPanel',
@@ -5006,6 +5014,11 @@
'PhabricatorSubscriptionsUIEventListener' => 'PhabricatorEventListener',
'PhabricatorSymbolNameLinter' => 'ArcanistXHPASTLintNamingHook',
'PhabricatorSyntaxHighlightingConfigOptions' => 'PhabricatorApplicationConfigOptions',
+ 'PhabricatorSystemActionEngine' => 'Phobject',
+ 'PhabricatorSystemActionGarbageCollector' => 'PhabricatorGarbageCollector',
+ 'PhabricatorSystemActionLog' => 'PhabricatorSystemDAO',
+ 'PhabricatorSystemActionRateLimitException' => 'Exception',
+ 'PhabricatorSystemDAO' => 'PhabricatorLiskDAO',
'PhabricatorTaskmasterDaemon' => 'PhabricatorDaemon',
'PhabricatorTestCase' => 'ArcanistPhutilTestCase',
'PhabricatorTestController' => 'PhabricatorController',
diff --git a/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php
--- a/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php
+++ b/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php
@@ -111,6 +111,23 @@
$user = new PhabricatorUser();
}
+ if ($ex instanceof PhabricatorSystemActionRateLimitException) {
+ $error_view = id(new AphrontErrorView())
+ ->setErrors(array(pht('You are being rate limited.')));
+
+ $dialog = id(new AphrontDialogView())
+ ->setTitle(pht('Slow Down!'))
+ ->setUser($user)
+ ->appendChild($error_view)
+ ->appendParagraph($ex->getMessage())
+ ->appendParagraph($ex->getRateExplanation())
+ ->addCancelButton('/', pht('Okaaaaaaaaaaaaaay...'));
+
+ $response = new AphrontDialogResponse();
+ $response->setDialog($dialog);
+ return $response;
+ }
+
if ($ex instanceof PhabricatorPolicyException) {
if (!$user->isLoggedIn()) {
diff --git a/src/applications/settings/action/PhabricatorSettingsAddEmailAction.php b/src/applications/settings/action/PhabricatorSettingsAddEmailAction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/settings/action/PhabricatorSettingsAddEmailAction.php
@@ -0,0 +1,20 @@
+<?php
+
+final class PhabricatorSettingsAddEmailAction extends PhabricatorSystemAction {
+
+ const TYPECONST = 'email.add';
+
+ public function getActionConstant() {
+ return self::TYPECONST;
+ }
+
+ public function getScoreThreshold() {
+ return 6 / phutil_units('1 hour in seconds');
+ }
+
+ public function getLimitExplanation() {
+ return pht(
+ 'You are adding too many email addresses to your account too quickly.');
+ }
+
+}
diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelEmailAddresses.php b/src/applications/settings/panel/PhabricatorSettingsPanelEmailAddresses.php
--- a/src/applications/settings/panel/PhabricatorSettingsPanelEmailAddresses.php
+++ b/src/applications/settings/panel/PhabricatorSettingsPanelEmailAddresses.php
@@ -171,6 +171,11 @@
return id(new AphrontReloadResponse())->setURI($uri);
}
+ PhabricatorSystemActionEngine::willTakeAction(
+ array($user->getPHID()),
+ new PhabricatorSettingsAddEmailAction(),
+ 1);
+
if (!strlen($email)) {
$e_email = pht('Required');
$errors[] = pht('Email is required.');
diff --git a/src/applications/system/action/PhabricatorSystemAction.php b/src/applications/system/action/PhabricatorSystemAction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/system/action/PhabricatorSystemAction.php
@@ -0,0 +1,40 @@
+<?php
+
+abstract class PhabricatorSystemAction {
+
+ abstract public function getActionConstant();
+ abstract public function getScoreThreshold();
+
+ public function shouldBlockActor($actor, $score) {
+ return ($score > $this->getScoreThreshold());
+ }
+
+ public function getLimitExplanation() {
+ return pht('You are performing too many actions too quickly.');
+ }
+
+ public function getRateExplanation($score) {
+ return pht(
+ 'The maximum allowed rate for this action is %s. You are taking '.
+ 'actions at a rate of %s.',
+ $this->formatRate($this->getScoreThreshold()),
+ $this->formatRate($score));
+ }
+
+ protected function formatRate($rate) {
+ if ($rate > 10) {
+ $str = pht('%d / second', $rate);
+ } else {
+ $rate *= 60;
+ if ($rate > 10) {
+ $str = pht('%d / minute', $rate);
+ } else {
+ $rate *= 60;
+ $str = pht('%d / hour', $rate);
+ }
+ }
+
+ return phutil_tag('strong', array(), $str);
+ }
+
+}
diff --git a/src/applications/system/engine/PhabricatorSystemActionEngine.php b/src/applications/system/engine/PhabricatorSystemActionEngine.php
new file mode 100644
--- /dev/null
+++ b/src/applications/system/engine/PhabricatorSystemActionEngine.php
@@ -0,0 +1,119 @@
+<?php
+
+final class PhabricatorSystemActionEngine extends Phobject {
+
+ public static function willTakeAction(
+ array $actors,
+ PhabricatorSystemAction $action,
+ $score) {
+
+ // If the score for this action is negative, we're giving the user a credit,
+ // so don't bother checking if they're blocked or not.
+ if ($score >= 0) {
+ $blocked = self::loadBlockedActors($actors, $action, $score);
+ if ($blocked) {
+ foreach ($blocked as $actor => $actor_score) {
+ throw new PhabricatorSystemActionRateLimitException(
+ $action,
+ $actor_score + ($score / self::getWindow()));
+ }
+ }
+ }
+
+ self::recordAction($actors, $action, $score);
+ }
+
+ public static function loadBlockedActors(
+ array $actors,
+ PhabricatorSystemAction $action) {
+
+ $scores = self::loadScores($actors, $action);
+
+ $blocked = array();
+ foreach ($scores as $actor => $score) {
+ if ($action->shouldBlockActor($actor, $score)) {
+ $blocked[$actor] = $score;
+ }
+ }
+
+ return $blocked;
+ }
+
+ public static function loadScores(
+ array $actors,
+ PhabricatorSystemAction $action) {
+
+ if (!$actors) {
+ return array();
+ }
+
+ $actor_hashes = array();
+ foreach ($actors as $actor) {
+ $actor_hashes[] = PhabricatorHash::digestForIndex($actor);
+ }
+
+ $log = new PhabricatorSystemActionLog();
+
+ $window = self::getWindow();
+
+ $conn_r = $log->establishConnection('r');
+ $scores = queryfx_all(
+ $conn_r,
+ 'SELECT actorIdentity, SUM(score) totalScore FROM %T
+ WHERE action = %s AND actorHash IN (%Ls)
+ AND epoch >= %d GROUP BY actorHash',
+ $log->getTableName(),
+ $action->getActionConstant(),
+ $actor_hashes,
+ (time() - $window));
+
+ $scores = ipull($scores, 'totalScore', 'actorIdentity');
+
+ foreach ($scores as $key => $score) {
+ $scores[$key] = $score / $window;
+ }
+
+ $scores = $scores + array_fill_keys($actors, 0);
+
+ return $scores;
+ }
+
+ private static function recordAction(
+ array $actors,
+ PhabricatorSystemAction $action,
+ $score) {
+
+ $log = new PhabricatorSystemActionLog();
+ $conn_w = $log->establishConnection('w');
+
+ $sql = array();
+ foreach ($actors as $actor) {
+ $sql[] = qsprintf(
+ $conn_w,
+ '(%s, %s, %s, %f, %d)',
+ PhabricatorHash::digestForIndex($actor),
+ $actor,
+ $action->getActionConstant(),
+ $score,
+ time());
+ }
+
+ foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
+ queryfx(
+ $conn_w,
+ 'INSERT INTO %T (actorHash, actorIdentity, action, score, epoch)
+ VALUES %Q',
+ $log->getTableName(),
+ $chunk);
+ }
+ }
+
+ private static function getWindow() {
+ // Limit queries to the last hour of data so we don't need to look at as
+ // many rows. We can use an arbitrarily larger window instead (we normalize
+ // scores to actions per second) but all the actions we care about limiting
+ // have a limit much higher than one action per hour.
+ return phutil_units('1 hour in seconds');
+ }
+
+}
diff --git a/src/applications/system/exception/PhabricatorSystemActionRateLimitException.php b/src/applications/system/exception/PhabricatorSystemActionRateLimitException.php
new file mode 100644
--- /dev/null
+++ b/src/applications/system/exception/PhabricatorSystemActionRateLimitException.php
@@ -0,0 +1,18 @@
+<?php
+
+final class PhabricatorSystemActionRateLimitException extends Exception {
+
+ private $action;
+ private $score;
+
+ public function __construct(PhabricatorSystemAction $action, $score) {
+ $this->action = $action;
+ $this->score = $score;
+ parent::__construct($action->getLimitExplanation());
+ }
+
+ public function getRateExplanation() {
+ return $this->action->getRateExplanation($this->score);
+ }
+
+}
diff --git a/src/applications/system/garbagecollector/PhabricatorSystemActionGarbageCollector.php b/src/applications/system/garbagecollector/PhabricatorSystemActionGarbageCollector.php
new file mode 100644
--- /dev/null
+++ b/src/applications/system/garbagecollector/PhabricatorSystemActionGarbageCollector.php
@@ -0,0 +1,21 @@
+<?php
+
+final class PhabricatorSystemActionGarbageCollector
+ extends PhabricatorGarbageCollector {
+
+ public function collectGarbage() {
+ $ttl = phutil_units('3 days in seconds');
+
+ $table = new PhabricatorSystemActionLog();
+ $conn_w = $table->establishConnection('w');
+
+ queryfx(
+ $conn_w,
+ 'DELETE FROM %T WHERE epoch < %d LIMIT 100',
+ $table->getTableName(),
+ time() - $ttl);
+
+ return ($conn_w->getAffectedRows() == 100);
+ }
+
+}
diff --git a/src/applications/system/storage/PhabricatorSystemActionLog.php b/src/applications/system/storage/PhabricatorSystemActionLog.php
new file mode 100644
--- /dev/null
+++ b/src/applications/system/storage/PhabricatorSystemActionLog.php
@@ -0,0 +1,22 @@
+<?php
+
+final class PhabricatorSystemActionLog extends PhabricatorSystemDAO {
+
+ protected $actorHash;
+ protected $actorIdentity;
+ protected $action;
+ protected $score;
+ protected $epoch;
+
+ public function getConfiguration() {
+ return array(
+ self::CONFIG_TIMESTAMPS => false,
+ ) + parent::getConfiguration();
+ }
+
+ public function setActorIdentity($identity) {
+ $this->setActorHash(PhabricatorHash::digestForIndex($identity));
+ return parent::setActorIdentity($identity);
+ }
+
+}
diff --git a/src/applications/system/storage/PhabricatorSystemDAO.php b/src/applications/system/storage/PhabricatorSystemDAO.php
new file mode 100644
--- /dev/null
+++ b/src/applications/system/storage/PhabricatorSystemDAO.php
@@ -0,0 +1,9 @@
+<?php
+
+abstract class PhabricatorSystemDAO extends PhabricatorLiskDAO {
+
+ public function getApplicationName() {
+ return 'system';
+ }
+
+}
diff --git a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php
--- a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php
+++ b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php
@@ -118,6 +118,7 @@
'db.passphrase' => array(),
'db.phragment' => array(),
'db.dashboard' => array(),
+ 'db.system' => array(),
'0000.legacy.sql' => array(
'legacy' => 0,
),
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Tue, Oct 15, 6:35 AM (4 w, 18 h ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6711726
Default Alt Text
D8683.id.diff (14 KB)
Attached To
Mode
D8683: Add semi-generic rate limiting infrastructure
Attached
Detach File
Event Timeline
Log In to Comment