Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php
index 8da9b22b23..5a56efb255 100644
--- a/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php
+++ b/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php
@@ -1,310 +1,312 @@
<?php
/**
* NOTE: Do not extend this!
*
* @concrete-extensible
* @group aphront
*/
class AphrontDefaultApplicationConfiguration
extends AphrontApplicationConfiguration {
public function __construct() {
}
public function getApplicationName() {
return 'aphront-default';
}
public function getURIMap() {
return $this->getResourceURIMapRules() + array(
'/~/' => array(
'' => 'DarkConsoleController',
'data/(?P<key>[^/]+)/' => 'DarkConsoleDataController',
),
);
}
protected function getResourceURIMapRules() {
return array(
'/res/' => array(
'(?:(?P<mtime>[0-9]+)T/)?'.
'(?P<library>[^/]+)/'.
'(?P<hash>[a-f0-9]{8})/'.
'(?P<path>.+\.(?:css|js|jpg|png|swf|gif|woff))'
=> 'CelerityPhabricatorResourceController',
),
);
}
/**
* @phutil-external-symbol class PhabricatorStartup
*/
public function buildRequest() {
$parser = new PhutilQueryStringParser();
$data = array();
// If the request has "multipart/form-data" content, we can't use
// PhutilQueryStringParser to parse it, and the raw data supposedly is not
// available anyway (according to the PHP documentation, "php://input" is
// not available for "multipart/form-data" requests). However, it is
// available at least some of the time (see T3673), so double check that
// we aren't trying to parse data we won't be able to parse correctly by
// examining the Content-Type header.
$content_type = idx($_SERVER, 'CONTENT_TYPE');
$is_form_data = preg_match('@^multipart/form-data@i', $content_type);
$raw_input = PhabricatorStartup::getRawInput();
if (strlen($raw_input) && !$is_form_data) {
$data += $parser->parseQueryString($raw_input);
} else if ($_POST) {
$data += $_POST;
}
$data += $parser->parseQueryString(idx($_SERVER, 'QUERY_STRING', ''));
$cookie_prefix = PhabricatorEnv::getEnvConfig('phabricator.cookie-prefix');
$request = new AphrontRequest($this->getHost(), $this->getPath());
$request->setRequestData($data);
$request->setApplicationConfiguration($this);
$request->setCookiePrefix($cookie_prefix);
return $request;
}
public function handleException(Exception $ex) {
$request = $this->getRequest();
// For Conduit requests, return a Conduit response.
if ($request->isConduit()) {
$response = new ConduitAPIResponse();
$response->setErrorCode(get_class($ex));
$response->setErrorInfo($ex->getMessage());
return id(new AphrontJSONResponse())
->setAddJSONShield(false)
->setContent($response->toDictionary());
}
// For non-workflow requests, return a Ajax response.
if ($request->isAjax() && !$request->isJavelinWorkflow()) {
// Log these; they don't get shown on the client and can be difficult
// to debug.
phlog($ex);
$response = new AphrontAjaxResponse();
$response->setError(
array(
'code' => get_class($ex),
'info' => $ex->getMessage(),
));
return $response;
}
$user = $request->getUser();
if (!$user) {
// If we hit an exception very early, we won't have a user.
$user = new PhabricatorUser();
}
if ($ex instanceof PhabricatorSystemActionRateLimitException) {
$dialog = id(new AphrontDialogView())
->setTitle(pht('Slow Down!'))
->setUser($user)
->setErrors(array(pht('You are being rate limited.')))
->appendParagraph($ex->getMessage())
->appendParagraph($ex->getRateExplanation())
->addCancelButton('/', pht('Okaaaaaaaaaaaaaay...'));
$response = new AphrontDialogResponse();
$response->setDialog($dialog);
return $response;
}
if ($ex instanceof PhabricatorAuthHighSecurityRequiredException) {
$form = id(new PhabricatorAuthSessionEngine())->renderHighSecurityForm(
+ $ex->getFactors(),
+ $ex->getFactorValidationResults(),
$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()) {
// If the user isn't logged in, just give them a login form. This is
// probably a generally more useful response than a policy dialog that
// they have to click through to get a login form.
//
// Possibly we should add a header here like "you need to login to see
// the thing you are trying to look at".
$login_controller = new PhabricatorAuthStartController($request);
$auth_app_class = 'PhabricatorApplicationAuth';
$auth_app = PhabricatorApplication::getByClass($auth_app_class);
$login_controller->setCurrentApplication($auth_app);
return $login_controller->processRequest();
}
$list = $ex->getMoreInfo();
foreach ($list as $key => $item) {
$list[$key] = phutil_tag('li', array(), $item);
}
if ($list) {
$list = phutil_tag('ul', array(), $list);
}
$content = array(
phutil_tag(
'div',
array(
'class' => 'aphront-policy-rejection',
),
$ex->getRejection()),
phutil_tag(
'div',
array(
'class' => 'aphront-capability-details',
),
pht('Users with the "%s" capability:', $ex->getCapabilityName())),
$list,
);
$dialog = new AphrontDialogView();
$dialog
->setTitle($ex->getTitle())
->setClass('aphront-access-dialog')
->setUser($user)
->appendChild($content);
if ($this->getRequest()->isAjax()) {
$dialog->addCancelButton('/', pht('Close'));
} else {
$dialog->addCancelButton('/', pht('OK'));
}
$response = new AphrontDialogResponse();
$response->setDialog($dialog);
return $response;
}
if ($ex instanceof AphrontUsageException) {
$error = new AphrontErrorView();
$error->setTitle($ex->getTitle());
$error->appendChild($ex->getMessage());
$view = new PhabricatorStandardPageView();
$view->setRequest($this->getRequest());
$view->appendChild($error);
$response = new AphrontWebpageResponse();
$response->setContent($view->render());
$response->setHTTPResponseCode(500);
return $response;
}
// Always log the unhandled exception.
phlog($ex);
$class = get_class($ex);
$message = $ex->getMessage();
if ($ex instanceof AphrontQuerySchemaException) {
$message .=
"\n\n".
"NOTE: This usually indicates that the MySQL schema has not been ".
"properly upgraded. Run 'bin/storage upgrade' to ensure your ".
"schema is up to date.";
}
if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) {
$trace = id(new AphrontStackTraceView())
->setUser($user)
->setTrace($ex->getTrace());
} else {
$trace = null;
}
$content = phutil_tag(
'div',
array('class' => 'aphront-unhandled-exception'),
array(
phutil_tag('div', array('class' => 'exception-message'), $message),
$trace,
));
$dialog = new AphrontDialogView();
$dialog
->setTitle('Unhandled Exception ("'.$class.'")')
->setClass('aphront-exception-dialog')
->setUser($user)
->appendChild($content);
if ($this->getRequest()->isAjax()) {
$dialog->addCancelButton('/', 'Close');
}
$response = new AphrontDialogResponse();
$response->setDialog($dialog);
$response->setHTTPResponseCode(500);
return $response;
}
public function willSendResponse(AphrontResponse $response) {
return $response;
}
public function build404Controller() {
return array(new Phabricator404Controller($this->getRequest()), array());
}
public function buildRedirectController($uri) {
return array(
new PhabricatorRedirectController($this->getRequest()),
array(
'uri' => $uri,
));
}
}
diff --git a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php
index 9cffd4812e..9a1b1036be 100644
--- a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php
+++ b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php
@@ -1,328 +1,371 @@
<?php
/**
* @task hisec High Security Mode
*/
final class PhabricatorAuthSessionEngine extends Phobject {
/**
* Session issued to normal users after they login through a standard channel.
* Associates the client with a standard user identity.
*/
const KIND_USER = 'U';
/**
* Session issued to users who login with some sort of credentials but do not
* have full accounts. These are sometimes called "grey users".
*
* TODO: We do not currently issue these sessions, see T4310.
*/
const KIND_EXTERNAL = 'X';
/**
* Session issued to logged-out users which has no real identity information.
* Its purpose is to protect logged-out users from CSRF.
*/
const KIND_ANONYMOUS = 'A';
/**
* Session kind isn't known.
*/
const KIND_UNKNOWN = '?';
/**
* Get the session kind (e.g., anonymous, user, external account) from a
* session token. Returns a `KIND_` constant.
*
* @param string Session token.
* @return const Session kind constant.
*/
public static function getSessionKindFromToken($session_token) {
if (strpos($session_token, '/') === false) {
// Old-style session, these are all user sessions.
return self::KIND_USER;
}
list($kind, $key) = explode('/', $session_token, 2);
switch ($kind) {
case self::KIND_ANONYMOUS:
case self::KIND_USER:
case self::KIND_EXTERNAL:
return $kind;
default:
return self::KIND_UNKNOWN;
}
}
public function loadUserForSession($session_type, $session_token) {
$session_kind = self::getSessionKindFromToken($session_token);
switch ($session_kind) {
case self::KIND_ANONYMOUS:
// Don't bother trying to load a user for an anonymous session, since
// neither the session nor the user exist.
return null;
case self::KIND_UNKNOWN:
// If we don't know what kind of session this is, don't go looking for
// it.
return null;
case self::KIND_USER:
break;
case self::KIND_EXTERNAL:
// TODO: Implement these (T4310).
return null;
}
$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. This might be getting too clever
// for its own good, though...
$info = queryfx_one(
$conn_r,
'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,
$session_key);
if (!$info) {
return null;
}
$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);
// If more than 20% of the time on this session has been used, refresh the
// 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) > $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->getTableName(),
$ttl,
$session->getID());
unset($unguarded);
}
$user = $user_table->loadFromArray($info);
$user->attachSession($session);
return $user;
}
/**
* Issue a new session key for a given identity. Phabricator supports
* different types of sessions (like "web" and "conduit") and each session
* type may have multiple concurrent sessions (this allows a user to be
* logged in on multiple browsers at the same time, for instance).
*
* Note that this method is transport-agnostic and does not set cookies or
* issue other types of tokens, it ONLY generates a new session key.
*
* You can configure the maximum number of concurrent sessions for various
* session types in the Phabricator configuration.
*
* @param const Session type constant (see
* @{class:PhabricatorAuthSession}).
* @param phid|null Identity to establish a session for, usually a user
* PHID. With `null`, generates an anonymous session.
* @return string Newly generated session key.
*/
public function establishSession($session_type, $identity_phid) {
// Consume entropy to generate a new session key, forestalling the eventual
// heat death of the universe.
$session_key = Filesystem::readRandomCharacters(40);
if ($identity_phid === null) {
return self::KIND_ANONYMOUS.'/'.$session_key;
}
$session_table = new PhabricatorAuthSession();
$conn_w = $session_table->establishConnection('w');
// This has a side effect of validating the session type.
$session_ttl = PhabricatorAuthSession::getSessionTypeTTL($session_type);
// Logging-in users don't have CSRF stuff yet, so we have to unguard this
// write.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
id(new PhabricatorAuthSession())
->setUserPHID($identity_phid)
->setType($session_type)
->setSessionKey(PhabricatorHash::digest($session_key))
->setSessionStart(time())
->setSessionExpires(time() + $session_ttl)
->save();
$log = PhabricatorUserLog::initializeNewLog(
null,
$identity_phid,
PhabricatorUserLog::ACTION_LOGIN);
$log->setDetails(
array(
'session_type' => $session_type,
));
$log->setSession($session_key);
$log->save();
unset($unguarded);
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();
+ // Check if the session is already in high security mode.
$token = $this->issueHighSecurityToken($session);
if ($token) {
return $token;
}
+ // Load the multi-factor auth sources attached to this account.
+ $factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
+ 'userPHID = %s',
+ $viewer->getPHID());
+
+ // If the account has no associated multi-factor auth, just issue a token
+ // without putting the session into high security mode. This is generally
+ // easier for users. A minor but desirable side effect is that when a user
+ // adds an auth factor, existing sessions won't get a free pass into hisec,
+ // since they never actually got marked as hisec.
+ if (!$factors) {
+ return $this->issueHighSecurityToken($session, true);
+ }
+
+ $validation_results = array();
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());
-
- $log = PhabricatorUserLog::initializeNewLog(
- $viewer,
- $viewer->getPHID(),
- PhabricatorUserLog::ACTION_ENTER_HISEC);
- $log->save();
+ $ok = true;
+ foreach ($factors as $factor) {
+ $id = $factor->getID();
+ $impl = $factor->requireImplementation();
+
+ $validation_results[$id] = $impl->processValidateFactorForm(
+ $factor,
+ $viewer,
+ $request);
+
+ if (!$impl->isFactorValid($factor, $validation_results[$id])) {
+ $ok = false;
+ }
+ }
+
+ if ($ok) {
+ $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());
+
+ $log = PhabricatorUserLog::initializeNewLog(
+ $viewer,
+ $viewer->getPHID(),
+ PhabricatorUserLog::ACTION_ENTER_HISEC);
+ $log->save();
+ } else {
+ $log = PhabricatorUserLog::initializeNewLog(
+ $viewer,
+ $viewer->getPHID(),
+ PhabricatorUserLog::ACTION_FAIL_HISEC);
+ $log->save();
+ }
}
}
$token = $this->issueHighSecurityToken($session);
if ($token) {
return $token;
}
throw id(new PhabricatorAuthHighSecurityRequiredException())
- ->setCancelURI($cancel_uri);
+ ->setCancelURI($cancel_uri)
+ ->setFactors($factors)
+ ->setFactorValidationResults($validation_results);
}
/**
* 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(
+ array $factors,
+ array $validation_results,
PhabricatorUser $viewer,
AphrontRequest $request) {
- // TODO: This is stubbed.
-
$form = id(new AphrontFormView())
->setUser($viewer)
- ->appendRemarkupInstructions('')
- ->appendChild(
- id(new AphrontFormTextControl())
- ->setLabel(pht('Secret Stuff')))
->appendRemarkupInstructions('');
+ foreach ($factors as $factor) {
+ $factor->requireImplementation()->renderValidateFactorForm(
+ $factor,
+ $form,
+ $viewer,
+ idx($validation_results, $factor->getID()));
+ }
+
+ $form->appendRemarkupInstructions('');
+
return $form;
}
public function exitHighSecurity(
PhabricatorUser $viewer,
PhabricatorAuthSession $session) {
queryfx(
$session->establishConnection('w'),
'UPDATE %T SET highSecurityUntil = NULL WHERE id = %d',
$session->getTableName(),
$session->getID());
$log = PhabricatorUserLog::initializeNewLog(
$viewer,
$viewer->getPHID(),
PhabricatorUserLog::ACTION_EXIT_HISEC);
$log->save();
}
}
diff --git a/src/applications/auth/exception/PhabricatorAuthHighSecurityRequiredException.php b/src/applications/auth/exception/PhabricatorAuthHighSecurityRequiredException.php
index 16022e53b8..56a4f9fc89 100644
--- a/src/applications/auth/exception/PhabricatorAuthHighSecurityRequiredException.php
+++ b/src/applications/auth/exception/PhabricatorAuthHighSecurityRequiredException.php
@@ -1,16 +1,37 @@
<?php
final class PhabricatorAuthHighSecurityRequiredException extends Exception {
private $cancelURI;
+ private $factors;
+ private $factorValidationResults;
+
+ public function setFactorValidationResults(array $results) {
+ $this->factorValidationResults = $results;
+ return $this;
+ }
+
+ public function getFactorValidationResults() {
+ return $this->factorValidationResults;
+ }
+
+ public function setFactors(array $factors) {
+ assert_instances_of($factors, 'PhabricatorAuthFactorConfig');
+ $this->factors = $factors;
+ return $this;
+ }
+
+ public function getFactors() {
+ return $this->factors;
+ }
public function setCancelURI($cancel_uri) {
$this->cancelURI = $cancel_uri;
return $this;
}
public function getCancelURI() {
return $this->cancelURI;
}
}
diff --git a/src/applications/auth/factor/PhabricatorAuthFactor.php b/src/applications/auth/factor/PhabricatorAuthFactor.php
index 75599200eb..f579667ce8 100644
--- a/src/applications/auth/factor/PhabricatorAuthFactor.php
+++ b/src/applications/auth/factor/PhabricatorAuthFactor.php
@@ -1,51 +1,74 @@
<?php
abstract class PhabricatorAuthFactor extends Phobject {
abstract public function getFactorName();
abstract public function getFactorKey();
abstract public function getFactorDescription();
abstract public function processAddFactorForm(
AphrontFormView $form,
AphrontRequest $request,
PhabricatorUser $user);
+ abstract public function renderValidateFactorForm(
+ PhabricatorAuthFactorConfig $config,
+ AphrontFormView $form,
+ PhabricatorUser $viewer,
+ $validation_result);
+
+ abstract public function processValidateFactorForm(
+ PhabricatorAuthFactorConfig $config,
+ PhabricatorUser $viewer,
+ AphrontRequest $request);
+
+ public function isFactorValid(
+ PhabricatorAuthFactorConfig $config,
+ $validation_result) {
+ return (idx($validation_result, 'valid') === true);
+ }
+
+ public function getParameterName(
+ PhabricatorAuthFactorConfig $config,
+ $name) {
+ return 'authfactor.'.$config->getID().'.'.$name;
+ }
+
public static function getAllFactors() {
static $factors;
if ($factors === null) {
$map = id(new PhutilSymbolLoader())
->setAncestorClass(__CLASS__)
->loadObjects();
$factors = array();
foreach ($map as $factor) {
$key = $factor->getFactorKey();
if (empty($factors[$key])) {
$factors[$key] = $factor;
} else {
$this_class = get_class($factor);
$that_class = get_class($factors[$key]);
throw new Exception(
pht(
'Two auth factors (with classes "%s" and "%s") both provide '.
'implementations with the same key ("%s"). Each factor must '.
'have a unique key.',
$this_class,
$that_class,
$key));
}
}
}
return $factors;
}
protected function newConfigForUser(PhabricatorUser $user) {
return id(new PhabricatorAuthFactorConfig())
->setUserPHID($user->getPHID())
->setFactorKey($this->getFactorKey());
}
}
diff --git a/src/applications/auth/factor/PhabricatorAuthFactorTOTP.php b/src/applications/auth/factor/PhabricatorAuthFactorTOTP.php
index cc09126368..2d00d220fe 100644
--- a/src/applications/auth/factor/PhabricatorAuthFactorTOTP.php
+++ b/src/applications/auth/factor/PhabricatorAuthFactorTOTP.php
@@ -1,179 +1,222 @@
<?php
final class PhabricatorAuthFactorTOTP extends PhabricatorAuthFactor {
public function getFactorKey() {
return 'totp';
}
public function getFactorName() {
return pht('Mobile Phone App (TOTP)');
}
public function getFactorDescription() {
return pht(
'Attach a mobile authenticator application (like Authy '.
'or Google Authenticator) to your account. When you need to '.
'authenticate, you will enter a code shown on your phone.');
}
public function processAddFactorForm(
AphrontFormView $form,
AphrontRequest $request,
PhabricatorUser $user) {
$key = $request->getStr('totpkey');
if (!strlen($key)) {
// TODO: When the user submits a key, we should require that it be
// one we generated for them, so there's no way an attacker can ever
// force a key they control onto an account. However, it's clumsy to
// do this right now. Once we have one-time tokens for SMS and email,
// we should be able to put it on that infrastructure.
$key = self::generateNewTOTPKey();
}
$code = $request->getStr('totpcode');
$e_code = true;
if ($request->getExists('totp')) {
$okay = self::verifyTOTPCode(
$user,
new PhutilOpaqueEnvelope($key),
$code);
if ($okay) {
$config = $this->newConfigForUser($user)
->setFactorName(pht('Mobile App (TOTP)'))
->setFactorSecret($key);
return $config;
} else {
if (!strlen($code)) {
$e_code = pht('Required');
} else {
$e_code = pht('Invalid');
}
}
}
$form->addHiddenInput('totp', true);
$form->addHiddenInput('totpkey', $key);
$form->appendRemarkupInstructions(
pht(
'First, download an authenticator application on your phone. Two '.
'applications which work well are **Authy** and **Google '.
'Authenticator**, but any other TOTP application should also work.'));
$form->appendInstructions(
pht(
'Launch the application on your phone, and add a new entry for '.
'this Phabricator install. When prompted, enter the key shown '.
'below into the application.'));
$form->appendChild(
id(new AphrontFormStaticControl())
->setLabel(pht('Key'))
->setValue(phutil_tag('strong', array(), $key)));
$form->appendInstructions(
pht(
'(If given an option, select that this key is "Time Based", not '.
'"Counter Based".)'));
$form->appendInstructions(
pht(
'After entering the key, the application should display a numeric '.
'code. Enter that code below to confirm that you have configured '.
'the authenticator correctly:'));
$form->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('TOTP Code'))
->setName('totpcode')
->setValue($code)
->setError($e_code));
}
+ public function renderValidateFactorForm(
+ PhabricatorAuthFactorConfig $config,
+ AphrontFormView $form,
+ PhabricatorUser $viewer,
+ $validation_result) {
+
+ if (!$validation_result) {
+ $validation_result = array();
+ }
+
+ $form->appendChild(
+ id(new AphrontFormTextControl())
+ ->setName($this->getParameterName($config, 'totpcode'))
+ ->setLabel(pht('App Code'))
+ ->setCaption(pht('Factor Name: %s', $config->getFactorName()))
+ ->setValue(idx($validation_result, 'value'))
+ ->setError(idx($validation_result, 'error', true)));
+ }
+
+ public function processValidateFactorForm(
+ PhabricatorAuthFactorConfig $config,
+ PhabricatorUser $viewer,
+ AphrontRequest $request) {
+
+ $code = $request->getStr($this->getParameterName($config, 'totpcode'));
+ $key = new PhutilOpaqueEnvelope($config->getFactorSecret());
+
+ if (self::verifyTOTPCode($viewer, $key, $code)) {
+ return array(
+ 'error' => null,
+ 'value' => $code,
+ 'valid' => true,
+ );
+ } else {
+ return array(
+ 'error' => strlen($code) ? pht('Invalid') : pht('Required'),
+ 'value' => $code,
+ 'valid' => false,
+ );
+ }
+ }
+
+
public static function generateNewTOTPKey() {
return strtoupper(Filesystem::readRandomCharacters(16));
}
public static function verifyTOTPCode(
PhabricatorUser $user,
PhutilOpaqueEnvelope $key,
$code) {
// TODO: This should use rate limiting to prevent multiple attempts in a
// short period of time.
$now = (int)(time() / 30);
// Allow the user to enter a code a few minutes away on either side, in
// case the server or client has some clock skew.
for ($offset = -2; $offset <= 2; $offset++) {
$real = self::getTOTPCode($key, $now + $offset);
if ($real === $code) {
return true;
}
}
// TODO: After validating a code, this should mark it as used and prevent
// it from being reused.
return false;
}
public static function base32Decode($buf) {
$buf = strtoupper($buf);
$map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
$map = str_split($map);
$map = array_flip($map);
$out = '';
$len = strlen($buf);
$acc = 0;
$bits = 0;
for ($ii = 0; $ii < $len; $ii++) {
$chr = $buf[$ii];
$val = $map[$chr];
$acc = $acc << 5;
$acc = $acc + $val;
$bits += 5;
if ($bits >= 8) {
$bits = $bits - 8;
$out .= chr(($acc & (0xFF << $bits)) >> $bits);
}
}
return $out;
}
public static function getTOTPCode(PhutilOpaqueEnvelope $key, $timestamp) {
$binary_timestamp = pack('N*', 0).pack('N*', $timestamp);
$binary_key = self::base32Decode($key->openEnvelope());
$hash = hash_hmac('sha1', $binary_timestamp, $binary_key, true);
// See RFC 4226.
$offset = ord($hash[19]) & 0x0F;
$code = ((ord($hash[$offset + 0]) & 0x7F) << 24) |
((ord($hash[$offset + 1]) & 0xFF) << 16) |
((ord($hash[$offset + 2]) & 0xFF) << 8) |
((ord($hash[$offset + 3]) ) );
$code = ($code % 1000000);
$code = str_pad($code, 6, '0', STR_PAD_LEFT);
return $code;
}
}
diff --git a/src/applications/auth/storage/PhabricatorAuthFactorConfig.php b/src/applications/auth/storage/PhabricatorAuthFactorConfig.php
index cc2d7d447b..6e22a10e0d 100644
--- a/src/applications/auth/storage/PhabricatorAuthFactorConfig.php
+++ b/src/applications/auth/storage/PhabricatorAuthFactorConfig.php
@@ -1,29 +1,42 @@
<?php
final class PhabricatorAuthFactorConfig extends PhabricatorAuthDAO {
protected $userPHID;
protected $factorKey;
protected $factorName;
protected $factorSecret;
protected $properties = array();
public function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_AUX_PHID => true,
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorAuthPHIDTypeAuthFactor::TYPECONST);
}
public function getImplementation() {
return idx(PhabricatorAuthFactor::getAllFactors(), $this->getFactorKey());
}
+ public function requireImplementation() {
+ $impl = $this->getImplementation();
+ if (!$impl) {
+ throw new Exception(
+ pht(
+ 'Attempting to operate on multi-factor auth which has no '.
+ 'corresponding implementation (factor key is "%s").',
+ $this->getFactorKey()));
+ }
+
+ return $impl;
+ }
+
}
diff --git a/src/applications/people/storage/PhabricatorUserLog.php b/src/applications/people/storage/PhabricatorUserLog.php
index c61e10fbe7..74f399595c 100644
--- a/src/applications/people/storage/PhabricatorUserLog.php
+++ b/src/applications/people/storage/PhabricatorUserLog.php
@@ -1,172 +1,174 @@
<?php
final class PhabricatorUserLog extends PhabricatorUserDAO
implements PhabricatorPolicyInterface {
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_EDIT = 'edit';
const ACTION_ADMIN = 'admin';
const ACTION_SYSTEM_AGENT = 'system-agent';
const ACTION_DISABLE = 'disable';
const ACTION_APPROVE = 'approve';
const ACTION_DELETE = 'delete';
const ACTION_CONDUIT_CERTIFICATE = 'conduit-cert';
const ACTION_CONDUIT_CERTIFICATE_FAILURE = 'conduit-cert-fail';
const ACTION_EMAIL_PRIMARY = 'email-primary';
const ACTION_EMAIL_REMOVE = 'email-remove';
const ACTION_EMAIL_ADD = 'email-add';
const ACTION_CHANGE_PASSWORD = 'change-password';
const ACTION_CHANGE_USERNAME = 'change-username';
const ACTION_ENTER_HISEC = 'hisec-enter';
const ACTION_EXIT_HISEC = 'hisec-exit';
+ const ACTION_FAIL_HISEC = 'hisec-fail';
const ACTION_MULTI_ADD = 'multi-add';
const ACTION_MULTI_REMOVE = 'multi-remove';
protected $actorPHID;
protected $userPHID;
protected $action;
protected $oldValue;
protected $newValue;
protected $details = array();
protected $remoteAddr;
protected $session;
public static function getActionTypeMap() {
return array(
self::ACTION_LOGIN => pht('Login'),
self::ACTION_LOGIN_FAILURE => pht('Login Failure'),
self::ACTION_LOGOUT => pht('Logout'),
self::ACTION_RESET_PASSWORD => pht('Reset Password'),
self::ACTION_CREATE => pht('Create Account'),
self::ACTION_EDIT => pht('Edit Account'),
self::ACTION_ADMIN => pht('Add/Remove Administrator'),
self::ACTION_SYSTEM_AGENT => pht('Add/Remove System Agent'),
self::ACTION_DISABLE => pht('Enable/Disable'),
self::ACTION_APPROVE => pht('Approve Registration'),
self::ACTION_DELETE => pht('Delete User'),
self::ACTION_CONDUIT_CERTIFICATE
=> pht('Conduit: Read Certificate'),
self::ACTION_CONDUIT_CERTIFICATE_FAILURE
=> pht('Conduit: Read Certificate Failure'),
self::ACTION_EMAIL_PRIMARY => pht('Email: Change Primary'),
self::ACTION_EMAIL_ADD => pht('Email: Add Address'),
self::ACTION_EMAIL_REMOVE => pht('Email: Remove Address'),
self::ACTION_CHANGE_PASSWORD => pht('Change Password'),
self::ACTION_CHANGE_USERNAME => pht('Change Username'),
self::ACTION_ENTER_HISEC => pht('Hisec: Enter'),
self::ACTION_EXIT_HISEC => pht('Hisec: Exit'),
+ self::ACTION_FAIL_HISEC => pht('Hisec: Failed Attempt'),
self::ACTION_MULTI_ADD => pht('Multi-Factor: Add Factor'),
self::ACTION_MULTI_REMOVE => pht('Multi-Factor: Remove Factor'),
);
}
public static function initializeNewLog(
PhabricatorUser $actor = null,
$object_phid,
$action) {
$log = new PhabricatorUserLog();
if ($actor) {
$log->setActorPHID($actor->getPHID());
if ($actor->hasSession()) {
$session = $actor->getSession();
// NOTE: This is a hash of the real session value, so it's safe to
// store it directly in the logs.
$log->setSession($session->getSessionKey());
}
}
$log->setUserPHID((string)$object_phid);
$log->setAction($action);
$log->remoteAddr = idx($_SERVER, 'REMOTE_ADDR', '');
return $log;
}
public static function loadRecentEventsFromThisIP($action, $timespan) {
return id(new PhabricatorUserLog())->loadAllWhere(
'action = %s AND remoteAddr = %s AND dateCreated > %d
ORDER BY dateCreated DESC',
$action,
idx($_SERVER, 'REMOTE_ADDR'),
time() - $timespan);
}
public function save() {
$this->details['host'] = php_uname('n');
$this->details['user_agent'] = AphrontRequest::getHTTPHeader('User-Agent');
return parent::save();
}
public function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'oldValue' => self::SERIALIZATION_JSON,
'newValue' => self::SERIALIZATION_JSON,
'details' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::POLICY_NOONE;
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
if ($viewer->getIsAdmin()) {
return true;
}
$viewer_phid = $viewer->getPHID();
if ($viewer_phid) {
$user_phid = $this->getUserPHID();
if ($viewer_phid == $user_phid) {
return true;
}
$actor_phid = $this->getActorPHID();
if ($viewer_phid == $actor_phid) {
return true;
}
}
return false;
}
public function describeAutomaticCapability($capability) {
return array(
pht('Users can view their activity and activity that affects them.'),
pht('Administrators can always view all activity.'),
);
}
}
diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelMultiFactor.php b/src/applications/settings/panel/PhabricatorSettingsPanelMultiFactor.php
index ba8fe22da0..0ba72962c1 100644
--- a/src/applications/settings/panel/PhabricatorSettingsPanelMultiFactor.php
+++ b/src/applications/settings/panel/PhabricatorSettingsPanelMultiFactor.php
@@ -1,309 +1,304 @@
<?php
final class PhabricatorSettingsPanelMultiFactor
extends PhabricatorSettingsPanel {
public function getPanelKey() {
return 'multifactor';
}
public function getPanelName() {
return pht('Multi-Factor Auth');
}
public function getPanelGroup() {
return pht('Authentication');
}
- public function isEnabled() {
- // TODO: Enable this panel once more pieces work correctly.
- return false;
- }
-
public function processRequest(AphrontRequest $request) {
if ($request->getExists('new')) {
return $this->processNew($request);
}
if ($request->getExists('edit')) {
return $this->processEdit($request);
}
if ($request->getExists('delete')) {
return $this->processDelete($request);
}
$user = $this->getUser();
$viewer = $request->getUser();
$factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
'userPHID = %s',
$user->getPHID());
$rows = array();
$rowc = array();
$highlight_id = $request->getInt('id');
foreach ($factors as $factor) {
$impl = $factor->getImplementation();
if ($impl) {
$type = $impl->getFactorName();
} else {
$type = $factor->getFactorKey();
}
if ($factor->getID() == $highlight_id) {
$rowc[] = 'highlighted';
} else {
$rowc[] = null;
}
$rows[] = array(
javelin_tag(
'a',
array(
'href' => $this->getPanelURI('?edit='.$factor->getID()),
'sigil' => 'workflow',
),
$factor->getFactorName()),
$type,
phabricator_datetime($factor->getDateCreated(), $viewer),
javelin_tag(
'a',
array(
'href' => $this->getPanelURI('?delete='.$factor->getID()),
'sigil' => 'workflow',
'class' => 'small grey button',
),
pht('Remove')),
);
}
$table = new AphrontTableView($rows);
$table->setNoDataString(
pht("You haven't added any authentication factors to your account yet."));
$table->setHeaders(
array(
pht('Name'),
pht('Type'),
pht('Created'),
'',
));
$table->setColumnClasses(
array(
'wide pri',
'',
'right',
'action',
));
$table->setRowClasses($rowc);
$table->setDeviceVisibility(
array(
true,
false,
false,
true,
));
$panel = new PHUIObjectBoxView();
$header = new PHUIHeaderView();
$create_icon = id(new PHUIIconView())
->setSpriteSheet(PHUIIconView::SPRITE_ICONS)
->setSpriteIcon('new');
$create_button = id(new PHUIButtonView())
->setText(pht('Add Authentication Factor'))
->setHref($this->getPanelURI('?new=true'))
->setTag('a')
->setWorkflow(true)
->setIcon($create_icon);
$header->setHeader(pht('Authentication Factors'));
$header->addActionLink($create_button);
$panel->setHeader($header);
$panel->appendChild($table);
return $panel;
}
private function processNew(AphrontRequest $request) {
$viewer = $request->getUser();
$user = $this->getUser();
$token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
$viewer,
$request,
$this->getPanelURI());
$factors = PhabricatorAuthFactor::getAllFactors();
$form = id(new AphrontFormView())
->setUser($viewer);
$type = $request->getStr('type');
if (empty($factors[$type]) || !$request->isFormPost()) {
$factor = null;
} else {
$factor = $factors[$type];
}
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->addHiddenInput('new', true);
if ($factor === null) {
$choice_control = id(new AphrontFormRadioButtonControl())
->setName('type')
->setValue(key($factors));
foreach ($factors as $available_factor) {
$choice_control->addButton(
$available_factor->getFactorKey(),
$available_factor->getFactorName(),
$available_factor->getFactorDescription());
}
$dialog->appendParagraph(
pht(
'Adding an additional authentication factor increases the security '.
'of your account.'));
$form
->appendChild($choice_control);
} else {
$dialog->addHiddenInput('type', $type);
$config = $factor->processAddFactorForm(
$form,
$request,
$user);
if ($config) {
$config->save();
$log = PhabricatorUserLog::initializeNewLog(
$viewer,
$user->getPHID(),
PhabricatorUserLog::ACTION_MULTI_ADD);
$log->save();
return id(new AphrontRedirectResponse())
->setURI($this->getPanelURI('?id='.$config->getID()));
}
}
$dialog
->setWidth(AphrontDialogView::WIDTH_FORM)
->setTitle(pht('Add Authentication Factor'))
->appendChild($form->buildLayoutView())
->addSubmitButton(pht('Continue'))
->addCancelButton($this->getPanelURI());
return id(new AphrontDialogResponse())
->setDialog($dialog);
}
private function processEdit(AphrontRequest $request) {
$viewer = $request->getUser();
$user = $this->getUser();
$factor = id(new PhabricatorAuthFactorConfig())->loadOneWhere(
'id = %d AND userPHID = %s',
$request->getInt('edit'),
$user->getPHID());
if (!$factor) {
return new Aphront404Response();
}
$e_name = true;
$errors = array();
if ($request->isFormPost()) {
$name = $request->getStr('name');
if (!strlen($name)) {
$e_name = pht('Required');
$errors[] = pht(
'Authentication factors must have a name to identify them.');
}
if (!$errors) {
$factor->setFactorName($name);
$factor->save();
return id(new AphrontRedirectResponse())
->setURI($this->getPanelURI('?id='.$factor->getID()));
}
} else {
$name = $factor->getFactorName();
}
$form = id(new AphrontFormView())
->setUser($viewer)
->appendChild(
id(new AphrontFormTextControl())
->setName('name')
->setLabel(pht('Name'))
->setValue($name)
->setError($e_name));
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->addHiddenInput('edit', $factor->getID())
->setTitle(pht('Edit Authentication Factor'))
->setErrors($errors)
->appendChild($form->buildLayoutView())
->addSubmitButton(pht('Save'))
->addCancelButton($this->getPanelURI());
return id(new AphrontDialogResponse())
->setDialog($dialog);
}
private function processDelete(AphrontRequest $request) {
$viewer = $request->getUser();
$user = $this->getUser();
$token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
$viewer,
$request,
$this->getPanelURI());
$factor = id(new PhabricatorAuthFactorConfig())->loadOneWhere(
'id = %d AND userPHID = %s',
$request->getInt('delete'),
$user->getPHID());
if (!$factor) {
return new Aphront404Response();
}
if ($request->isFormPost()) {
$factor->delete();
$log = PhabricatorUserLog::initializeNewLog(
$viewer,
$user->getPHID(),
PhabricatorUserLog::ACTION_MULTI_REMOVE);
$log->save();
return id(new AphrontRedirectResponse())
->setURI($this->getPanelURI());
}
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->addHiddenInput('delete', $factor->getID())
->setTitle(pht('Delete Authentication Factor'))
->appendParagraph(
pht(
'Really remove the authentication factor %s from your account?',
phutil_tag('strong', array(), $factor->getFactorName())))
->addSubmitButton(pht('Remove Factor'))
->addCancelButton($this->getPanelURI());
return id(new AphrontDialogResponse())
->setDialog($dialog);
}
}
diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php b/src/applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php
index 3af78bd169..8ab4a4da6a 100644
--- a/src/applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php
+++ b/src/applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php
@@ -1,425 +1,418 @@
<?php
final class PhabricatorSettingsPanelSSHKeys
extends PhabricatorSettingsPanel {
public function isEditableByAdministrators() {
return true;
}
public function getPanelKey() {
return 'ssh';
}
public function getPanelName() {
return pht('SSH Public Keys');
}
public function getPanelGroup() {
return pht('Authentication');
}
public function isEnabled() {
return true;
}
public function processRequest(AphrontRequest $request) {
$viewer = $request->getUser();
$user = $this->getUser();
$generate = $request->getStr('generate');
if ($generate) {
return $this->processGenerate($request);
}
$edit = $request->getStr('edit');
$delete = $request->getStr('delete');
if (!$edit && !$delete) {
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/');
-
- */
+ $this->getPanelURI());
$id = nonempty($edit, $delete);
if ($id && is_numeric($id)) {
// NOTE: This prevents editing/deleting of keys not owned by the user.
$key = id(new PhabricatorUserSSHKey())->loadOneWhere(
'userPHID = %s AND id = %d',
$user->getPHID(),
(int)$id);
if (!$key) {
return new Aphront404Response();
}
} else {
$key = new PhabricatorUserSSHKey();
$key->setUserPHID($user->getPHID());
}
if ($delete) {
return $this->processDelete($request, $key);
}
$e_name = true;
$e_key = true;
$errors = array();
$entire_key = $key->getEntireKey();
if ($request->isFormPost()) {
$key->setName($request->getStr('name'));
$entire_key = $request->getStr('key');
if (!strlen($entire_key)) {
$errors[] = pht('You must provide an SSH Public Key.');
$e_key = pht('Required');
} else {
try {
list($type, $body, $comment) = self::parsePublicKey($entire_key);
$key->setKeyType($type);
$key->setKeyBody($body);
$key->setKeyHash(md5($body));
$key->setKeyComment($comment);
$e_key = null;
} catch (Exception $ex) {
$e_key = pht('Invalid');
$errors[] = $ex->getMessage();
}
}
if (!strlen($key->getName())) {
$errors[] = pht('You must name this public key.');
$e_name = pht('Required');
} else {
$e_name = null;
}
if (!$errors) {
try {
$key->save();
return id(new AphrontRedirectResponse())
->setURI($this->getPanelURI());
} catch (AphrontQueryDuplicateKeyException $ex) {
$e_key = pht('Duplicate');
$errors[] = pht('This public key is already associated with a user '.
'account.');
}
}
}
$is_new = !$key->getID();
if ($is_new) {
$header = pht('Add New SSH Public Key');
$save = pht('Add Key');
} else {
$header = pht('Edit SSH Public Key');
$save = pht('Save Changes');
}
$form = id(new AphrontFormView())
->setUser($viewer)
->addHiddenInput('edit', $is_new ? 'true' : $key->getID())
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Name'))
->setName('name')
->setValue($key->getName())
->setError($e_name))
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel(pht('Public Key'))
->setName('key')
->setValue($entire_key)
->setError($e_key))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($this->getPanelURI())
->setValue($save));
$form_box = id(new PHUIObjectBoxView())
->setHeaderText($header)
->setFormErrors($errors)
->setForm($form);
return $form_box;
}
private function renderKeyListView(AphrontRequest $request) {
$user = $this->getUser();
$viewer = $request->getUser();
$keys = id(new PhabricatorUserSSHKey())->loadAllWhere(
'userPHID = %s',
$user->getPHID());
$rows = array();
foreach ($keys as $key) {
$rows[] = array(
phutil_tag(
'a',
array(
'href' => $this->getPanelURI('?edit='.$key->getID()),
),
$key->getName()),
$key->getKeyComment(),
$key->getKeyType(),
phabricator_date($key->getDateCreated(), $viewer),
phabricator_time($key->getDateCreated(), $viewer),
javelin_tag(
'a',
array(
'href' => $this->getPanelURI('?delete='.$key->getID()),
'class' => 'small grey button',
'sigil' => 'workflow',
),
pht('Delete')),
);
}
$table = new AphrontTableView($rows);
$table->setNoDataString(pht("You haven't added any SSH Public Keys."));
$table->setHeaders(
array(
pht('Name'),
pht('Comment'),
pht('Type'),
pht('Created'),
pht('Time'),
'',
));
$table->setColumnClasses(
array(
'wide pri',
'',
'',
'',
'right',
'action',
));
$panel = new PHUIObjectBoxView();
$header = new PHUIHeaderView();
$upload_icon = id(new PHUIIconView())
->setSpriteSheet(PHUIIconView::SPRITE_ICONS)
->setSpriteIcon('upload');
$upload_button = id(new PHUIButtonView())
->setText(pht('Upload Public Key'))
->setHref($this->getPanelURI('?edit=true'))
->setTag('a')
->setIcon($upload_icon);
try {
PhabricatorSSHKeyGenerator::assertCanGenerateKeypair();
$can_generate = true;
} catch (Exception $ex) {
$can_generate = false;
}
$generate_icon = id(new PHUIIconView())
->setSpriteSheet(PHUIIconView::SPRITE_ICONS)
->setSpriteIcon('lock');
$generate_button = id(new PHUIButtonView())
->setText(pht('Generate Keypair'))
->setHref($this->getPanelURI('?generate=true'))
->setTag('a')
->setWorkflow(true)
->setDisabled(!$can_generate)
->setIcon($generate_icon);
$header->setHeader(pht('SSH Public Keys'));
$header->addActionLink($generate_button);
$header->addActionLink($upload_button);
$panel->setHeader($header);
$panel->appendChild($table);
return $panel;
}
private function processDelete(
AphrontRequest $request,
PhabricatorUserSSHKey $key) {
$viewer = $request->getUser();
$user = $this->getUser();
$name = phutil_tag('strong', array(), $key->getName());
if ($request->isDialogFormPost()) {
$key->delete();
return id(new AphrontReloadResponse())
->setURI($this->getPanelURI());
}
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->addHiddenInput('delete', $key->getID())
->setTitle(pht('Really delete SSH Public Key?'))
->appendChild(phutil_tag('p', array(), pht(
'The key "%s" will be permanently deleted, and you will not longer be '.
'able to use the corresponding private key to authenticate.',
$name)))
->addSubmitButton(pht('Delete Public Key'))
->addCancelButton($this->getPanelURI());
return id(new AphrontDialogResponse())
->setDialog($dialog);
}
private function processGenerate(AphrontRequest $request) {
$user = $this->getUser();
$viewer = $request->getUser();
$is_self = ($user->getPHID() == $viewer->getPHID());
if ($request->isFormPost()) {
$keys = PhabricatorSSHKeyGenerator::generateKeypair();
list($public_key, $private_key) = $keys;
$file = PhabricatorFile::buildFromFileDataOrHash(
$private_key,
array(
'name' => 'id_rsa_phabricator.key',
'ttl' => time() + (60 * 10),
'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
));
list($type, $body, $comment) = self::parsePublicKey($public_key);
$key = id(new PhabricatorUserSSHKey())
->setUserPHID($user->getPHID())
->setName('id_rsa_phabricator')
->setKeyType($type)
->setKeyBody($body)
->setKeyHash(md5($body))
->setKeyComment(pht('Generated'))
->save();
// NOTE: We're disabling workflow on submit so the download works. We're
// disabling workflow on cancel so the page reloads, showing the new
// key.
if ($is_self) {
$what_happened = pht(
'The public key has been associated with your Phabricator '.
'account. Use the button below to download the private key.');
} else {
$what_happened = pht(
'The public key has been associated with the %s account. '.
'Use the button below to download the private key.',
phutil_tag('strong', array(), $user->getUsername()));
}
$dialog = id(new AphrontDialogView())
->setTitle(pht('Download Private Key'))
->setUser($viewer)
->setDisableWorkflowOnCancel(true)
->setDisableWorkflowOnSubmit(true)
->setSubmitURI($file->getDownloadURI())
->appendParagraph(
pht(
'Successfully generated a new keypair.'))
->appendParagraph($what_happened)
->appendParagraph(
pht(
'After you download the private key, it will be destroyed. '.
'You will not be able to retrieve it if you lose your copy.'))
->addSubmitButton(pht('Download Private Key'))
->addCancelButton($this->getPanelURI(), pht('Done'));
return id(new AphrontDialogResponse())
->setDialog($dialog);
}
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->addCancelButton($this->getPanelURI());
try {
PhabricatorSSHKeyGenerator::assertCanGenerateKeypair();
if ($is_self) {
$explain = pht(
'This will generate an SSH keypair, associate the public key '.
'with your account, and let you download the private key.');
} else {
$explain = pht(
'This will generate an SSH keypair, associate the public key with '.
'the %s account, and let you download the private key.',
phutil_tag('strong', array(), $user->getUsername()));
}
$dialog
->addHiddenInput('generate', true)
->setTitle(pht('Generate New Keypair'))
->appendParagraph($explain)
->appendParagraph(
pht(
"Phabricator will not retain a copy of the private key."))
->addSubmitButton(pht('Generate Keypair'));
} catch (Exception $ex) {
$dialog
->setTitle(pht('Unable to Generate Keys'))
->appendParagraph($ex->getMessage());
}
return id(new AphrontDialogResponse())
->setDialog($dialog);
}
private static function parsePublicKey($entire_key) {
$parts = str_replace("\n", '', trim($entire_key));
$parts = preg_split('/\s+/', $parts);
if (count($parts) == 2) {
$parts[] = ''; // Add an empty comment part.
} else if (count($parts) == 3) {
// This is the expected case.
} else {
if (preg_match('/private\s*key/i', $entire_key)) {
// Try to give the user a better error message if it looks like
// they uploaded a private key.
throw new Exception(
pht('Provide your public key, not your private key!'));
} else {
throw new Exception(
pht('Provided public key is not properly formatted.'));
}
}
list($type, $body, $comment) = $parts;
$recognized_keys = array(
'ssh-dsa',
'ssh-dss',
'ssh-rsa',
'ecdsa-sha2-nistp256',
'ecdsa-sha2-nistp384',
'ecdsa-sha2-nistp521',
);
if (!in_array($type, $recognized_keys)) {
$type_list = implode(', ', $recognized_keys);
throw new Exception(
pht(
'Public key type should be one of: %s',
$type_list));
}
return array($type, $body, $comment);
}
}

File Metadata

Mime Type
text/x-diff
Expires
Mon, Jul 28, 8:21 PM (1 w, 3 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
187471
Default Alt Text
(61 KB)

Event Timeline