Page MenuHomePhabricator

D8851.diff
No OneTemporary

D8851.diff

diff --git a/resources/sql/autopatches/20140423.session.1.hisec.sql b/resources/sql/autopatches/20140423.session.1.hisec.sql
new file mode 100644
--- /dev/null
+++ b/resources/sql/autopatches/20140423.session.1.hisec.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_user.phabricator_session
+ ADD highSecurityUntil INT UNSIGNED;
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
@@ -1206,7 +1206,10 @@
'PhabricatorAuthController' => 'applications/auth/controller/PhabricatorAuthController.php',
'PhabricatorAuthDAO' => 'applications/auth/storage/PhabricatorAuthDAO.php',
'PhabricatorAuthDisableController' => 'applications/auth/controller/config/PhabricatorAuthDisableController.php',
+ 'PhabricatorAuthDowngradeSessionController' => 'applications/auth/controller/PhabricatorAuthDowngradeSessionController.php',
'PhabricatorAuthEditController' => 'applications/auth/controller/config/PhabricatorAuthEditController.php',
+ 'PhabricatorAuthHighSecurityRequiredException' => 'applications/auth/exception/PhabricatorAuthHighSecurityRequiredException.php',
+ 'PhabricatorAuthHighSecurityToken' => 'applications/auth/data/PhabricatorAuthHighSecurityToken.php',
'PhabricatorAuthLinkController' => 'applications/auth/controller/PhabricatorAuthLinkController.php',
'PhabricatorAuthListController' => 'applications/auth/controller/config/PhabricatorAuthListController.php',
'PhabricatorAuthLoginController' => 'applications/auth/controller/PhabricatorAuthLoginController.php',
@@ -3947,7 +3950,9 @@
'PhabricatorAuthController' => 'PhabricatorController',
'PhabricatorAuthDAO' => 'PhabricatorLiskDAO',
'PhabricatorAuthDisableController' => 'PhabricatorAuthProviderConfigController',
+ 'PhabricatorAuthDowngradeSessionController' => 'PhabricatorAuthController',
'PhabricatorAuthEditController' => 'PhabricatorAuthProviderConfigController',
+ 'PhabricatorAuthHighSecurityRequiredException' => 'Exception',
'PhabricatorAuthLinkController' => 'PhabricatorAuthController',
'PhabricatorAuthListController' => 'PhabricatorAuthProviderConfigController',
'PhabricatorAuthLoginController' => 'PhabricatorAuthController',
diff --git a/src/aphront/AphrontRequest.php b/src/aphront/AphrontRequest.php
--- a/src/aphront/AphrontRequest.php
+++ b/src/aphront/AphrontRequest.php
@@ -18,6 +18,7 @@
const TYPE_WORKFLOW = '__wflow__';
const TYPE_CONTINUE = '__continue__';
const TYPE_PREVIEW = '__preview__';
+ const TYPE_HISEC = '__hisec__';
private $host;
private $path;
@@ -263,6 +264,7 @@
final public function isFormPost() {
$post = $this->getExists(self::TYPE_FORM) &&
+ !$this->getExists(self::TYPE_HISEC) &&
$this->isHTTPPost();
if (!$post) {
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
@@ -123,6 +123,49 @@
return $response;
}
+ if ($ex instanceof PhabricatorAuthHighSecurityRequiredException) {
+
+ $form = id(new PhabricatorAuthSessionEngine())->renderHighSecurityForm(
+ $user,
+ $request);
+
+ $dialog = id(new AphrontDialogView())
+ ->setUser($user)
+ ->setTitle(pht('Entering High Security'))
+ ->setShortTitle(pht('Security Checkpoint'))
+ ->setWidth(AphrontDialogView::WIDTH_FORM)
+ ->addHiddenInput(AphrontRequest::TYPE_HISEC, true)
+ ->setErrors(
+ array(
+ pht(
+ 'You are taking an action which requires you to enter '.
+ 'high security.'),
+ ))
+ ->appendParagraph(
+ pht(
+ 'High security mode helps protect your account from security '.
+ 'threats, like session theft or someone messing with your stuff '.
+ 'while you\'re grabbing a coffee. To enter high security mode, '.
+ 'confirm your credentials.'))
+ ->appendChild($form->buildLayoutView())
+ ->appendParagraph(
+ pht(
+ 'Your account will remain in high security mode for a short '.
+ 'period of time. When you are finished taking sensitive '.
+ 'actions, you should leave high security.'))
+ ->setSubmitURI($request->getPath())
+ ->addCancelButton($ex->getCancelURI())
+ ->addSubmitButton(pht('Enter High Security'));
+
+ foreach ($request->getPassthroughRequestParameters() as $key => $value) {
+ $dialog->addHiddenInput($key, $value);
+ }
+
+ $response = new AphrontDialogResponse();
+ $response->setDialog($dialog);
+ return $response;
+ }
+
if ($ex instanceof PhabricatorPolicyException) {
if (!$user->isLoggedIn()) {
diff --git a/src/applications/auth/application/PhabricatorApplicationAuth.php b/src/applications/auth/application/PhabricatorApplicationAuth.php
--- a/src/applications/auth/application/PhabricatorApplicationAuth.php
+++ b/src/applications/auth/application/PhabricatorApplicationAuth.php
@@ -88,6 +88,8 @@
=> 'PhabricatorAuthConfirmLinkController',
'session/terminate/(?P<id>[^/]+)/'
=> 'PhabricatorAuthTerminateSessionController',
+ 'session/downgrade/'
+ => 'PhabricatorAuthDowngradeSessionController',
),
'/oauth/(?P<provider>\w+)/login/'
diff --git a/src/applications/auth/controller/PhabricatorAuthDowngradeSessionController.php b/src/applications/auth/controller/PhabricatorAuthDowngradeSessionController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/auth/controller/PhabricatorAuthDowngradeSessionController.php
@@ -0,0 +1,52 @@
+<?php
+
+final class PhabricatorAuthDowngradeSessionController
+ extends PhabricatorAuthController {
+
+ public function processRequest() {
+ $request = $this->getRequest();
+ $viewer = $request->getUser();
+
+ $panel_uri = '/settings/panel/sessions/';
+
+ $session = $viewer->getSession();
+ if ($session->getHighSecurityUntil() < time()) {
+ return $this->newDialog()
+ ->setTitle(pht('Normal Security Restored'))
+ ->appendParagraph(
+ pht('Your session is no longer in high security.'))
+ ->addCancelButton($panel_uri, pht('Continue'));
+ }
+
+ if ($request->isFormPost()) {
+
+ queryfx(
+ $session->establishConnection('w'),
+ 'UPDATE %T SET highSecurityUntil = NULL WHERE id = %d',
+ $session->getTableName(),
+ $session->getID());
+
+ return id(new AphrontRedirectResponse())
+ ->setURI($this->getApplicationURI('session/downgrade/'));
+ }
+
+ return $this->newDialog()
+ ->setTitle(pht('Leaving High Security'))
+ ->appendParagraph(
+ pht(
+ 'Leave high security and return your session to normal '.
+ 'security levels?'))
+ ->appendParagraph(
+ pht(
+ 'If you leave high security, you will need to authenticate '.
+ 'again the next time you try to take a high security action.'))
+ ->appendParagraph(
+ pht(
+ 'On the plus side, that purple notification bubble will '.
+ 'disappear.'))
+ ->addSubmitButton(pht('Leave High Security'))
+ ->addCancelButton($panel_uri, pht('Stay in High Security'));
+ }
+
+
+}
diff --git a/src/applications/auth/data/PhabricatorAuthHighSecurityToken.php b/src/applications/auth/data/PhabricatorAuthHighSecurityToken.php
new file mode 100644
--- /dev/null
+++ b/src/applications/auth/data/PhabricatorAuthHighSecurityToken.php
@@ -0,0 +1,5 @@
+<?php
+
+final class PhabricatorAuthHighSecurityToken {
+
+}
diff --git a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php
--- a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php
+++ b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php
@@ -1,5 +1,8 @@
<?php
+/**
+ * @task hisec High Security Mode
+ */
final class PhabricatorAuthSessionEngine extends Phobject {
/**
@@ -78,28 +81,43 @@
$session_table = new PhabricatorAuthSession();
$user_table = new PhabricatorUser();
$conn_r = $session_table->establishConnection('r');
+ $session_key = PhabricatorHash::digest($session_token);
// NOTE: We're being clever here because this happens on every page load,
- // and by joining we can save a query.
+ // and by joining we can save a query. This might be getting too clever
+ // for its own good, though...
$info = queryfx_one(
$conn_r,
- 'SELECT s.sessionExpires AS _sessionExpires, s.id AS _sessionID, u.*
+ 'SELECT
+ s.id AS s_id,
+ s.sessionExpires AS s_sessionExpires,
+ s.sessionStart AS s_sessionStart,
+ s.highSecurityUntil AS s_highSecurityUntil,
+ u.*
FROM %T u JOIN %T s ON u.phid = s.userPHID
AND s.type = %s AND s.sessionKey = %s',
$user_table->getTableName(),
$session_table->getTableName(),
$session_type,
- PhabricatorHash::digest($session_token));
+ $session_key);
if (!$info) {
return null;
}
- $expires = $info['_sessionExpires'];
- $id = $info['_sessionID'];
- unset($info['_sessionExpires']);
- unset($info['_sessionID']);
+ $session_dict = array(
+ 'userPHID' => $info['phid'],
+ 'sessionKey' => $session_key,
+ 'type' => $session_type,
+ );
+ foreach ($info as $key => $value) {
+ if (strncmp($key, 's_', 2) === 0) {
+ unset($info[$key]);
+ $session_dict[substr($key, 2)] = $value;
+ }
+ }
+ $session = id(new PhabricatorAuthSession())->loadFromArray($session_dict);
$ttl = PhabricatorAuthSession::getSessionTypeTTL($session_type);
@@ -107,19 +125,21 @@
// TTL back up to the full duration. The idea here is that sessions are
// good forever if used regularly, but get GC'd when they fall out of use.
- if (time() + (0.80 * $ttl) > $expires) {
+ if (time() + (0.80 * $ttl) > $session->getSessionExpires()) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$conn_w = $session_table->establishConnection('w');
queryfx(
$conn_w,
'UPDATE %T SET sessionExpires = UNIX_TIMESTAMP() + %d WHERE id = %d',
- $session_table->getTableName(),
+ $session->getTableName(),
$ttl,
- $id);
+ $session->getID());
unset($unguarded);
}
- return $user_table->loadFromArray($info);
+ $user = $user_table->loadFromArray($info);
+ $user->attachSession($session);
+ return $user;
}
@@ -182,4 +202,104 @@
return $session_key;
}
+
+ /**
+ * Require high security, or prompt the user to enter high security.
+ *
+ * If the user's session is in high security, this method will return a
+ * token. Otherwise, it will throw an exception which will eventually
+ * be converted into a multi-factor authentication workflow.
+ *
+ * @param PhabricatorUser User whose session needs to be in high security.
+ * @param AphrontReqeust Current request.
+ * @param string URI to return the user to if they cancel.
+ * @return PhabricatorAuthHighSecurityToken Security token.
+ */
+ public function requireHighSecuritySession(
+ PhabricatorUser $viewer,
+ AphrontRequest $request,
+ $cancel_uri) {
+
+ if (!$viewer->hasSession()) {
+ throw new Exception(
+ pht('Requiring a high-security session from a user with no session!'));
+ }
+
+ $session = $viewer->getSession();
+
+ $token = $this->issueHighSecurityToken($session);
+ if ($token) {
+ return $token;
+ }
+
+ if ($request->isHTTPPost()) {
+ $request->validateCSRF();
+ if ($request->getExists(AphrontRequest::TYPE_HISEC)) {
+
+ // TODO: Actually verify that the user provided some multi-factor
+ // auth credentials here. For now, we just let you enter high
+ // security.
+
+ $until = time() + phutil_units('15 minutes in seconds');
+ $session->setHighSecurityUntil($until);
+
+ queryfx(
+ $session->establishConnection('w'),
+ 'UPDATE %T SET highSecurityUntil = %d WHERE id = %d',
+ $session->getTableName(),
+ $until,
+ $session->getID());
+ }
+ }
+
+ $token = $this->issueHighSecurityToken($session);
+ if ($token) {
+ return $token;
+ }
+
+ throw id(new PhabricatorAuthHighSecurityRequiredException())
+ ->setCancelURI($cancel_uri);
+ }
+
+
+ /**
+ * Issue a high security token for a session, if authorized.
+ *
+ * @param PhabricatorAuthSession Session to issue a token for.
+ * @return PhabricatorAuthHighSecurityToken|null Token, if authorized.
+ */
+ private function issueHighSecurityToken(PhabricatorAuthSession $session) {
+ $until = $session->getHighSecurityUntil();
+ if ($until > time()) {
+ return new PhabricatorAuthHighSecurityToken();
+ }
+ return null;
+ }
+
+
+ /**
+ * Render a form for providing relevant multi-factor credentials.
+ *
+ * @param PhabricatorUser Viewing user.
+ * @param AphrontRequest Current request.
+ * @return AphrontFormView Renderable form.
+ */
+ public function renderHighSecurityForm(
+ PhabricatorUser $viewer,
+ AphrontRequest $request) {
+
+ // TODO: This is stubbed.
+
+ $form = id(new AphrontFormView())
+ ->setUser($viewer)
+ ->appendRemarkupInstructions('')
+ ->appendChild(
+ id(new AphrontFormTextControl())
+ ->setLabel(pht('Secret Stuff')))
+ ->appendRemarkupInstructions('');
+
+ return $form;
+ }
+
+
}
diff --git a/src/applications/auth/exception/PhabricatorAuthHighSecurityRequiredException.php b/src/applications/auth/exception/PhabricatorAuthHighSecurityRequiredException.php
new file mode 100644
--- /dev/null
+++ b/src/applications/auth/exception/PhabricatorAuthHighSecurityRequiredException.php
@@ -0,0 +1,16 @@
+<?php
+
+final class PhabricatorAuthHighSecurityRequiredException extends Exception {
+
+ private $cancelURI;
+
+ public function setCancelURI($cancel_uri) {
+ $this->cancelURI = $cancel_uri;
+ return $this;
+ }
+
+ public function getCancelURI() {
+ return $this->cancelURI;
+ }
+
+}
diff --git a/src/applications/auth/storage/PhabricatorAuthSession.php b/src/applications/auth/storage/PhabricatorAuthSession.php
--- a/src/applications/auth/storage/PhabricatorAuthSession.php
+++ b/src/applications/auth/storage/PhabricatorAuthSession.php
@@ -11,6 +11,7 @@
protected $sessionKey;
protected $sessionStart;
protected $sessionExpires;
+ protected $highSecurityUntil;
private $identityObject = self::ATTACHABLE;
diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php
--- a/src/applications/people/storage/PhabricatorUser.php
+++ b/src/applications/people/storage/PhabricatorUser.php
@@ -42,6 +42,7 @@
private $customFields = self::ATTACHABLE;
private $alternateCSRFString = self::ATTACHABLE;
+ private $session = self::ATTACHABLE;
protected function readField($field) {
switch ($field) {
@@ -178,6 +179,19 @@
return $result;
}
+ public function attachSession(PhabricatorAuthSession $session) {
+ $this->session = $session;
+ return $this;
+ }
+
+ public function getSession() {
+ return $this->assertAttached($this->session);
+ }
+
+ public function hasSession() {
+ return ($this->session !== self::ATTACHABLE);
+ }
+
private function generateConduitCertificate() {
return Filesystem::readRandomCharacters(255);
}
diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php b/src/applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php
--- a/src/applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php
+++ b/src/applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php
@@ -38,6 +38,18 @@
return $this->renderKeyListView($request);
}
+ /*
+
+ NOTE: Uncomment this to test hisec.
+ TOOD: Implement this fully once hisec does something useful.
+
+ $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
+ $viewer,
+ $request,
+ '/settings/panel/ssh/');
+
+ */
+
$id = nonempty($edit, $delete);
if ($id && is_numeric($id)) {
diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelSessions.php b/src/applications/settings/panel/PhabricatorSettingsPanelSessions.php
--- a/src/applications/settings/panel/PhabricatorSettingsPanelSessions.php
+++ b/src/applications/settings/panel/PhabricatorSettingsPanelSessions.php
@@ -66,10 +66,15 @@
pht('Terminate'));
}
+ $hisec = ($session->getHighSecurityUntil() - time());
+
$rows[] = array(
$handles[$session->getUserPHID()]->renderLink(),
substr($session->getSessionKey(), 0, 6),
$session->getType(),
+ ($hisec > 0)
+ ? phabricator_format_relative_time($hisec)
+ : null,
phabricator_datetime($session->getSessionStart(), $viewer),
phabricator_date($session->getSessionExpires(), $viewer),
$button,
@@ -84,6 +89,7 @@
pht('Identity'),
pht('Session'),
pht('Type'),
+ pht('HiSec'),
pht('Created'),
pht('Expires'),
pht(''),
@@ -95,6 +101,7 @@
'',
'right',
'right',
+ 'right',
'action',
));
@@ -113,6 +120,20 @@
->setHeader(pht('Active Login Sessions'))
->addActionLink($terminate_button);
+ $hisec = ($viewer->getSession()->getHighSecurityUntil() - time());
+ if ($hisec > 0) {
+ $hisec_icon = id(new PHUIIconView())
+ ->setSpriteSheet(PHUIIconView::SPRITE_ICONS)
+ ->setSpriteIcon('lock');
+ $hisec_button = id(new PHUIButtonView())
+ ->setText(pht('Leave High Security'))
+ ->setHref('/auth/session/downgrade/')
+ ->setTag('a')
+ ->setWorkflow(true)
+ ->setIcon($hisec_icon);
+ $header->addActionLink($hisec_button);
+ }
+
$panel = id(new PHUIObjectBoxView())
->setHeader($header)
->appendChild($table);
diff --git a/src/view/page/PhabricatorStandardPageView.php b/src/view/page/PhabricatorStandardPageView.php
--- a/src/view/page/PhabricatorStandardPageView.php
+++ b/src/view/page/PhabricatorStandardPageView.php
@@ -168,6 +168,22 @@
Javelin::initBehavior('device');
+ if ($user->hasSession()) {
+ $hisec = ($user->getSession()->getHighSecurityUntil() - time());
+ if ($hisec > 0) {
+ $remaining_time = phabricator_format_relative_time($hisec);
+ Javelin::initBehavior(
+ 'high-security-warning',
+ array(
+ 'uri' => '/auth/session/downgrade/',
+ 'message' => pht(
+ 'Your session is in high security mode. When you '.
+ 'finish using it, click here to leave.',
+ $remaining_time),
+ ));
+ }
+ }
+
if ($console) {
require_celerity_resource('aphront-dark-console-css');
diff --git a/webroot/rsrc/css/aphront/notification.css b/webroot/rsrc/css/aphront/notification.css
--- a/webroot/rsrc/css/aphront/notification.css
+++ b/webroot/rsrc/css/aphront/notification.css
@@ -47,6 +47,11 @@
border: 1px solid {$red};
}
+.jx-notification-security {
+ background: {$lightviolet};
+ border: 1px solid {$violet};
+}
+
.jx-notification-container .phabricator-notification {
padding: 0;
}
diff --git a/webroot/rsrc/js/core/behavior-high-security-warning.js b/webroot/rsrc/js/core/behavior-high-security-warning.js
new file mode 100644
--- /dev/null
+++ b/webroot/rsrc/js/core/behavior-high-security-warning.js
@@ -0,0 +1,19 @@
+/**
+ * @provides javelin-behavior-high-security-warning
+ * @requires javelin-behavior
+ * javelin-uri
+ * phabricator-notification
+ */
+
+JX.behavior('high-security-warning', function(config) {
+
+ var n = new JX.Notification()
+ .setContent(config.message)
+ .setDuration(0)
+ .alterClassName('jx-notification-security', true);
+
+ n.listen('activate', function() { JX.$U(config.uri).go(); });
+
+ n.show();
+
+});

File Metadata

Mime Type
text/plain
Expires
Mon, Jun 3, 10:56 AM (3 w, 2 d ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6303255
Default Alt Text
D8851.diff (20 KB)

Event Timeline