Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/auth/controller/contact/PhabricatorAuthContactNumberPrimaryController.php b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberPrimaryController.php
index afcc065559..ee76094a68 100644
--- a/src/applications/auth/controller/contact/PhabricatorAuthContactNumberPrimaryController.php
+++ b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberPrimaryController.php
@@ -1,78 +1,87 @@
<?php
final class PhabricatorAuthContactNumberPrimaryController
extends PhabricatorAuthContactNumberController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$number = id(new PhabricatorAuthContactNumberQuery())
->setViewer($viewer)
->withIDs(array($id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$number) {
return new Aphront404Response();
}
$id = $number->getID();
$cancel_uri = $number->getURI();
if ($number->isDisabled()) {
return $this->newDialog()
->setTitle(pht('Number Disabled'))
->appendParagraph(
pht(
'You can not make a disabled number your primary contact number.'))
->addCancelButton($cancel_uri);
}
if ($number->getIsPrimary()) {
return $this->newDialog()
->setTitle(pht('Number Already Primary'))
->appendParagraph(
pht(
'This contact number is already your primary contact number.'))
->addCancelButton($cancel_uri);
}
if ($request->isFormPost()) {
$xactions = array();
$xactions[] = id(new PhabricatorAuthContactNumberTransaction())
->setTransactionType(
PhabricatorAuthContactNumberPrimaryTransaction::TRANSACTIONTYPE)
->setNewValue(true);
$editor = id(new PhabricatorAuthContactNumberEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true);
- $editor->applyTransactions($number, $xactions);
+ try {
+ $editor->applyTransactions($number, $xactions);
+ } catch (PhabricatorApplicationTransactionValidationException $ex) {
+ // This happens when you try to make a number into your primary
+ // number, but you have contact number MFA on your account.
+ return $this->newDialog()
+ ->setTitle(pht('Unable to Make Primary'))
+ ->setValidationException($ex)
+ ->addCancelButton($cancel_uri);
+ }
return id(new AphrontRedirectResponse())->setURI($cancel_uri);
}
$number_display = phutil_tag(
'strong',
array(),
$number->getDisplayName());
return $this->newDialog()
->setTitle(pht('Set Primary Contact Number'))
->appendParagraph(
pht(
'Designate %s as your primary contact number?',
$number_display))
->addSubmitButton(pht('Make Primary'))
->addCancelButton($cancel_uri);
}
}
diff --git a/src/applications/auth/factor/PhabricatorAuthFactor.php b/src/applications/auth/factor/PhabricatorAuthFactor.php
index 768ad34e18..f11b81549f 100644
--- a/src/applications/auth/factor/PhabricatorAuthFactor.php
+++ b/src/applications/auth/factor/PhabricatorAuthFactor.php
@@ -1,391 +1,403 @@
<?php
abstract class PhabricatorAuthFactor extends Phobject {
abstract public function getFactorName();
abstract public function getFactorKey();
abstract public function getFactorCreateHelp();
abstract public function getFactorDescription();
abstract public function processAddFactorForm(
PhabricatorAuthFactorProvider $provider,
AphrontFormView $form,
AphrontRequest $request,
PhabricatorUser $user);
abstract public function renderValidateFactorForm(
PhabricatorAuthFactorConfig $config,
AphrontFormView $form,
PhabricatorUser $viewer,
PhabricatorAuthFactorResult $validation_result);
public function getParameterName(
PhabricatorAuthFactorConfig $config,
$name) {
return 'authfactor.'.$config->getID().'.'.$name;
}
public static function getAllFactors() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getFactorKey')
->execute();
}
protected function newConfigForUser(PhabricatorUser $user) {
return id(new PhabricatorAuthFactorConfig())
->setUserPHID($user->getPHID())
->setFactorSecret('');
}
protected function newResult() {
return new PhabricatorAuthFactorResult();
}
public function newIconView() {
return id(new PHUIIconView())
->setIcon('fa-mobile');
}
public function canCreateNewProvider() {
return true;
}
public function getProviderCreateDescription() {
return null;
}
public function canCreateNewConfiguration(PhabricatorUser $user) {
return true;
}
public function getConfigurationCreateDescription(PhabricatorUser $user) {
return null;
}
+ /**
+ * Is this a factor which depends on the user's contact number?
+ *
+ * If a user has a "contact number" factor configured, they can not modify
+ * or switch their primary contact number.
+ *
+ * @return bool True if this factor should lock contact numbers.
+ */
+ public function isContactNumberFactor() {
+ return false;
+ }
+
abstract public function getEnrollDescription(
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $user);
public function getEnrollButtonText(
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $user) {
return pht('Continue');
}
public function getFactorOrder() {
return 1000;
}
final public function newSortVector() {
return id(new PhutilSortVector())
->addInt($this->canCreateNewProvider() ? 0 : 1)
->addInt($this->getFactorOrder())
->addString($this->getFactorName());
}
protected function newChallenge(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer) {
$engine = $config->getSessionEngine();
return PhabricatorAuthChallenge::initializeNewChallenge()
->setUserPHID($viewer->getPHID())
->setSessionPHID($viewer->getSession()->getPHID())
->setFactorPHID($config->getPHID())
->setWorkflowKey($engine->getWorkflowKey());
}
abstract public function getRequestHasChallengeResponse(
PhabricatorAuthFactorConfig $config,
AphrontRequest $response);
final public function getNewIssuedChallenges(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
array $challenges) {
assert_instances_of($challenges, 'PhabricatorAuthChallenge');
$now = PhabricatorTime::getNow();
// Factor implementations may need to perform writes in order to issue
// challenges, particularly push factors like SMS.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$new_challenges = $this->newIssuedChallenges(
$config,
$viewer,
$challenges);
assert_instances_of($new_challenges, 'PhabricatorAuthChallenge');
foreach ($new_challenges as $new_challenge) {
$ttl = $new_challenge->getChallengeTTL();
if (!$ttl) {
throw new Exception(
pht('Newly issued MFA challenges must have a valid TTL!'));
}
if ($ttl < $now) {
throw new Exception(
pht(
'Newly issued MFA challenges must have a future TTL. This '.
'factor issued a bad TTL ("%s"). (Did you use a relative '.
'time instead of an epoch?)',
$ttl));
}
}
foreach ($new_challenges as $challenge) {
$challenge->save();
}
unset($unguarded);
return $new_challenges;
}
abstract protected function newIssuedChallenges(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
array $challenges);
final public function getResultFromIssuedChallenges(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
array $challenges) {
assert_instances_of($challenges, 'PhabricatorAuthChallenge');
$result = $this->newResultFromIssuedChallenges(
$config,
$viewer,
$challenges);
if ($result === null) {
return $result;
}
if (!($result instanceof PhabricatorAuthFactorResult)) {
throw new Exception(
pht(
'Expected "newResultFromIssuedChallenges()" to return null or '.
'an object of class "%s"; got something else (in "%s").',
'PhabricatorAuthFactorResult',
get_class($this)));
}
$result->setIssuedChallenges($challenges);
return $result;
}
abstract protected function newResultFromIssuedChallenges(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
array $challenges);
final public function getResultFromChallengeResponse(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
AphrontRequest $request,
array $challenges) {
assert_instances_of($challenges, 'PhabricatorAuthChallenge');
$result = $this->newResultFromChallengeResponse(
$config,
$viewer,
$request,
$challenges);
if (!($result instanceof PhabricatorAuthFactorResult)) {
throw new Exception(
pht(
'Expected "newResultFromChallengeResponse()" to return an object '.
'of class "%s"; got something else (in "%s").',
'PhabricatorAuthFactorResult',
get_class($this)));
}
$result->setIssuedChallenges($challenges);
return $result;
}
abstract protected function newResultFromChallengeResponse(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
AphrontRequest $request,
array $challenges);
final protected function newAutomaticControl(
PhabricatorAuthFactorResult $result) {
$is_answered = (bool)$result->getAnsweredChallenge();
if ($is_answered) {
return $this->newAnsweredControl($result);
}
$is_wait = $result->getIsWait();
if ($is_wait) {
return $this->newWaitControl($result);
}
return null;
}
private function newWaitControl(
PhabricatorAuthFactorResult $result) {
$error = $result->getErrorMessage();
$icon = id(new PHUIIconView())
->setIcon('fa-clock-o', 'red');
return id(new PHUIFormTimerControl())
->setIcon($icon)
->appendChild($error)
->setError(pht('Wait'));
}
private function newAnsweredControl(
PhabricatorAuthFactorResult $result) {
$icon = id(new PHUIIconView())
->setIcon('fa-check-circle-o', 'green');
return id(new PHUIFormTimerControl())
->setIcon($icon)
->appendChild(
pht('You responded to this challenge correctly.'));
}
/* -( Synchronizing New Factors )------------------------------------------ */
final protected function loadMFASyncToken(
AphrontRequest $request,
AphrontFormView $form,
PhabricatorUser $user) {
// If the form included a synchronization key, load the corresponding
// token. The user must synchronize to a key we generated because this
// raises the barrier to theoretical attacks where an attacker might
// provide a known key for factors like TOTP.
// (We store and verify the hash of the key, not the key itself, to limit
// how useful the data in the table is to an attacker.)
$sync_type = PhabricatorAuthMFASyncTemporaryTokenType::TOKENTYPE;
$sync_token = null;
$sync_key = $request->getStr($this->getMFASyncTokenFormKey());
if (strlen($sync_key)) {
$sync_key_digest = PhabricatorHash::digestWithNamedKey(
$sync_key,
PhabricatorAuthMFASyncTemporaryTokenType::DIGEST_KEY);
$sync_token = id(new PhabricatorAuthTemporaryTokenQuery())
->setViewer($user)
->withTokenResources(array($user->getPHID()))
->withTokenTypes(array($sync_type))
->withExpired(false)
->withTokenCodes(array($sync_key_digest))
->executeOne();
}
if (!$sync_token) {
// Don't generate a new sync token if there are too many outstanding
// tokens already. This is mostly relevant for push factors like SMS,
// where generating a token has the side effect of sending a user a
// message.
$outstanding_limit = 10;
$outstanding_tokens = id(new PhabricatorAuthTemporaryTokenQuery())
->setViewer($user)
->withTokenResources(array($user->getPHID()))
->withTokenTypes(array($sync_type))
->withExpired(false)
->execute();
if (count($outstanding_tokens) > $outstanding_limit) {
throw new Exception(
pht(
'Your account has too many outstanding, incomplete MFA '.
'synchronization attempts. Wait an hour and try again.'));
}
$now = PhabricatorTime::getNow();
$sync_key = Filesystem::readRandomCharacters(32);
$sync_key_digest = PhabricatorHash::digestWithNamedKey(
$sync_key,
PhabricatorAuthMFASyncTemporaryTokenType::DIGEST_KEY);
$sync_ttl = $this->getMFASyncTokenTTL();
$sync_token = id(new PhabricatorAuthTemporaryToken())
->setIsNewTemporaryToken(true)
->setTokenResource($user->getPHID())
->setTokenType($sync_type)
->setTokenCode($sync_key_digest)
->setTokenExpires($now + $sync_ttl);
$properties = $this->newMFASyncTokenProperties($user);
foreach ($properties as $key => $value) {
$sync_token->setTemporaryTokenProperty($key, $value);
}
$sync_token->save();
}
$form->addHiddenInput($this->getMFASyncTokenFormKey(), $sync_key);
return $sync_token;
}
protected function newMFASyncTokenProperties(PhabricatorUser $user) {
return array();
}
private function getMFASyncTokenFormKey() {
return 'sync.key';
}
private function getMFASyncTokenTTL() {
return phutil_units('1 hour in seconds');
}
final protected function getChallengeForCurrentContext(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
array $challenges) {
$session_phid = $viewer->getSession()->getPHID();
$engine = $config->getSessionEngine();
$workflow_key = $engine->getWorkflowKey();
foreach ($challenges as $challenge) {
if ($challenge->getSessionPHID() !== $session_phid) {
continue;
}
if ($challenge->getWorkflowKey() !== $workflow_key) {
continue;
}
if ($challenge->getIsCompleted()) {
continue;
}
if ($challenge->getIsReusedChallenge()) {
continue;
}
return $challenge;
}
return null;
}
}
diff --git a/src/applications/auth/factor/PhabricatorSMSAuthFactor.php b/src/applications/auth/factor/PhabricatorSMSAuthFactor.php
index 03558f7333..baa714c21d 100644
--- a/src/applications/auth/factor/PhabricatorSMSAuthFactor.php
+++ b/src/applications/auth/factor/PhabricatorSMSAuthFactor.php
@@ -1,367 +1,371 @@
<?php
final class PhabricatorSMSAuthFactor
extends PhabricatorAuthFactor {
public function getFactorKey() {
return 'sms';
}
public function getFactorName() {
return pht('SMS');
}
public function getFactorCreateHelp() {
return pht(
'Allow users to receive a code via SMS.');
}
public function getFactorDescription() {
return pht(
'When you need to authenticate, a text message with a code will '.
'be sent to your phone.');
}
public function getFactorOrder() {
// Sort this factor toward the end of the list because SMS is relatively
// weak.
return 2000;
}
+ public function isContactNumberFactor() {
+ return true;
+ }
+
public function canCreateNewProvider() {
return $this->isSMSMailerConfigured();
}
public function getProviderCreateDescription() {
$messages = array();
if (!$this->isSMSMailerConfigured()) {
$messages[] = id(new PHUIInfoView())
->setErrors(
array(
pht(
'You have not configured an outbound SMS mailer. You must '.
'configure one before you can set up SMS. See: %s',
phutil_tag(
'a',
array(
'href' => '/config/edit/cluster.mailers/',
),
'cluster.mailers')),
));
}
$messages[] = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setErrors(
array(
pht(
'SMS is weak, and relatively easy for attackers to compromise. '.
'Strongly consider using a different MFA provider.'),
));
return $messages;
}
public function canCreateNewConfiguration(PhabricatorUser $user) {
if (!$this->loadUserContactNumber($user)) {
return false;
}
return true;
}
public function getConfigurationCreateDescription(PhabricatorUser $user) {
$messages = array();
if (!$this->loadUserContactNumber($user)) {
$messages[] = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setErrors(
array(
pht(
'You have not configured a primary contact number. Configure '.
'a contact number before adding SMS as an authentication '.
'factor.'),
));
}
return $messages;
}
public function getEnrollDescription(
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $user) {
return pht(
'To verify your phone as an authentication factor, a text message with '.
'a secret code will be sent to the phone number you have listed as '.
'your primary contact number.');
}
public function getEnrollButtonText(
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $user) {
$contact_number = $this->loadUserContactNumber($user);
return pht('Send SMS: %s', $contact_number->getDisplayName());
}
public function processAddFactorForm(
PhabricatorAuthFactorProvider $provider,
AphrontFormView $form,
AphrontRequest $request,
PhabricatorUser $user) {
$token = $this->loadMFASyncToken($request, $form, $user);
$code = $request->getStr('sms.code');
$e_code = true;
if (!$token->getIsNewTemporaryToken()) {
$expect_code = $token->getTemporaryTokenProperty('code');
$okay = phutil_hashes_are_identical(
$this->normalizeSMSCode($code),
$this->normalizeSMSCode($expect_code));
if ($okay) {
$config = $this->newConfigForUser($user)
->setFactorName(pht('SMS'));
return $config;
} else {
if (!strlen($code)) {
$e_code = pht('Required');
} else {
$e_code = pht('Invalid');
}
}
}
$form->appendRemarkupInstructions(
pht(
'Enter the code from the text message which was sent to your '.
'primary contact number.'));
$form->appendChild(
id(new PHUIFormNumberControl())
->setLabel(pht('SMS Code'))
->setName('sms.code')
->setValue($code)
->setError($e_code));
}
protected function newIssuedChallenges(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
array $challenges) {
// If we already issued a valid challenge for this workflow and session,
// don't issue a new one.
$challenge = $this->getChallengeForCurrentContext(
$config,
$viewer,
$challenges);
if ($challenge) {
return array();
}
// Otherwise, issue a new challenge.
$challenge_code = $this->newSMSChallengeCode();
$envelope = new PhutilOpaqueEnvelope($challenge_code);
$this->sendSMSCodeToUser($envelope, $viewer);
$ttl_seconds = phutil_units('15 minutes in seconds');
return array(
$this->newChallenge($config, $viewer)
->setChallengeKey($challenge_code)
->setChallengeTTL(PhabricatorTime::getNow() + $ttl_seconds),
);
}
protected function newResultFromIssuedChallenges(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
array $challenges) {
$challenge = $this->getChallengeForCurrentContext(
$config,
$viewer,
$challenges);
if ($challenge->getIsAnsweredChallenge()) {
return $this->newResult()
->setAnsweredChallenge($challenge);
}
return null;
}
public function renderValidateFactorForm(
PhabricatorAuthFactorConfig $config,
AphrontFormView $form,
PhabricatorUser $viewer,
PhabricatorAuthFactorResult $result) {
$control = $this->newAutomaticControl($result);
if (!$control) {
$value = $result->getValue();
$error = $result->getErrorMessage();
$name = $this->getChallengeResponseParameterName($config);
$control = id(new PHUIFormNumberControl())
->setName($name)
->setDisableAutocomplete(true)
->setValue($value)
->setError($error);
}
$control
->setLabel(pht('SMS Code'))
->setCaption(pht('Factor Name: %s', $config->getFactorName()));
$form->appendChild($control);
}
public function getRequestHasChallengeResponse(
PhabricatorAuthFactorConfig $config,
AphrontRequest $request) {
$value = $this->getChallengeResponseFromRequest($config, $request);
return (bool)strlen($value);
}
protected function newResultFromChallengeResponse(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
AphrontRequest $request,
array $challenges) {
$challenge = $this->getChallengeForCurrentContext(
$config,
$viewer,
$challenges);
$code = $this->getChallengeResponseFromRequest(
$config,
$request);
$result = $this->newResult()
->setValue($code);
if ($challenge->getIsAnsweredChallenge()) {
return $result->setAnsweredChallenge($challenge);
}
if (phutil_hashes_are_identical($code, $challenge->getChallengeKey())) {
$ttl = PhabricatorTime::getNow() + phutil_units('15 minutes in seconds');
$challenge
->markChallengeAsAnswered($ttl);
return $result->setAnsweredChallenge($challenge);
}
if (strlen($code)) {
$error_message = pht('Invalid');
} else {
$error_message = pht('Required');
}
$result->setErrorMessage($error_message);
return $result;
}
private function newSMSChallengeCode() {
$value = Filesystem::readRandomInteger(0, 99999999);
$value = sprintf('%08d', $value);
return $value;
}
private function isSMSMailerConfigured() {
$mailers = PhabricatorMetaMTAMail::newMailers(
array(
'outbound' => true,
'media' => array(
PhabricatorMailSMSMessage::MESSAGETYPE,
),
));
return (bool)$mailers;
}
private function loadUserContactNumber(PhabricatorUser $user) {
$contact_numbers = id(new PhabricatorAuthContactNumberQuery())
->setViewer($user)
->withObjectPHIDs(array($user->getPHID()))
->withStatuses(
array(
PhabricatorAuthContactNumber::STATUS_ACTIVE,
))
->withIsPrimary(true)
->execute();
if (count($contact_numbers) !== 1) {
return null;
}
return head($contact_numbers);
}
protected function newMFASyncTokenProperties(PhabricatorUser $user) {
$sms_code = $this->newSMSChallengeCode();
$envelope = new PhutilOpaqueEnvelope($sms_code);
$this->sendSMSCodeToUser($envelope, $user);
return array(
'code' => $sms_code,
);
}
private function sendSMSCodeToUser(
PhutilOpaqueEnvelope $envelope,
PhabricatorUser $user) {
$uri = PhabricatorEnv::getURI('/');
$uri = new PhutilURI($uri);
return id(new PhabricatorMetaMTAMail())
->setMessageType(PhabricatorMailSMSMessage::MESSAGETYPE)
->addTos(array($user->getPHID()))
->setForceDelivery(true)
->setSensitiveContent(true)
->setBody(
pht(
'Phabricator (%s) MFA Code: %s',
$uri->getDomain(),
$envelope->openEnvelope()))
->save();
}
private function normalizeSMSCode($code) {
return trim($code);
}
private function getChallengeResponseParameterName(
PhabricatorAuthFactorConfig $config) {
return $this->getParameterName($config, 'sms.code');
}
private function getChallengeResponseFromRequest(
PhabricatorAuthFactorConfig $config,
AphrontRequest $request) {
$name = $this->getChallengeResponseParameterName($config);
$value = $request->getStr($name);
$value = (string)$value;
$value = trim($value);
return $value;
}
}
diff --git a/src/applications/auth/xaction/PhabricatorAuthContactNumberNumberTransaction.php b/src/applications/auth/xaction/PhabricatorAuthContactNumberNumberTransaction.php
index 00499959ec..88d9d4bffc 100644
--- a/src/applications/auth/xaction/PhabricatorAuthContactNumberNumberTransaction.php
+++ b/src/applications/auth/xaction/PhabricatorAuthContactNumberNumberTransaction.php
@@ -1,91 +1,96 @@
<?php
final class PhabricatorAuthContactNumberNumberTransaction
extends PhabricatorAuthContactNumberTransactionType {
const TRANSACTIONTYPE = 'number';
public function generateOldValue($object) {
return $object->getContactNumber();
}
public function generateNewValue($object, $value) {
$number = new PhabricatorPhoneNumber($value);
return $number->toE164();
}
public function applyInternalEffects($object, $value) {
$object->setContactNumber($value);
}
public function getTitle() {
$old = $this->getOldValue();
$new = $this->getNewValue();
return pht(
'%s changed this contact number from %s to %s.',
$this->renderAuthor(),
$this->renderOldValue(),
$this->renderNewValue());
}
public function validateTransactions($object, array $xactions) {
$errors = array();
$current_value = $object->getContactNumber();
if ($this->isEmptyTextTransaction($current_value, $xactions)) {
$errors[] = $this->newRequiredError(
pht('Contact numbers must have a contact number.'));
return $errors;
}
$max_length = $object->getColumnMaximumByteLength('contactNumber');
foreach ($xactions as $xaction) {
$new_value = $xaction->getNewValue();
$new_length = strlen($new_value);
if ($new_length > $max_length) {
$errors[] = $this->newInvalidError(
pht(
'Contact numbers can not be longer than %s characters.',
new PhutilNumber($max_length)),
$xaction);
continue;
}
try {
new PhabricatorPhoneNumber($new_value);
} catch (Exception $ex) {
$errors[] = $this->newInvalidError(
pht(
'Contact number is invalid: %s',
$ex->getMessage()),
$xaction);
continue;
}
$new_value = $this->generateNewValue($object, $new_value);
$unique_key = id(clone $object)
->setContactNumber($new_value)
->newUniqueKey();
$other = id(new PhabricatorAuthContactNumberQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withUniqueKeys(array($unique_key))
->executeOne();
if ($other) {
if ($other->getID() !== $object->getID()) {
$errors[] = $this->newInvalidError(
pht('Contact number is already in use.'),
$xaction);
continue;
}
}
+ $mfa_error = $this->newContactNumberMFAError($object, $xaction);
+ if ($mfa_error) {
+ $errors[] = $mfa_error;
+ continue;
+ }
}
return $errors;
}
}
diff --git a/src/applications/auth/xaction/PhabricatorAuthContactNumberPrimaryTransaction.php b/src/applications/auth/xaction/PhabricatorAuthContactNumberPrimaryTransaction.php
index 2e4b6ff55c..42788029b5 100644
--- a/src/applications/auth/xaction/PhabricatorAuthContactNumberPrimaryTransaction.php
+++ b/src/applications/auth/xaction/PhabricatorAuthContactNumberPrimaryTransaction.php
@@ -1,49 +1,55 @@
<?php
final class PhabricatorAuthContactNumberPrimaryTransaction
extends PhabricatorAuthContactNumberTransactionType {
const TRANSACTIONTYPE = 'primary';
public function generateOldValue($object) {
return (bool)$object->getIsPrimary();
}
public function applyInternalEffects($object, $value) {
$object->setIsPrimary((int)$value);
}
public function getTitle() {
return pht(
'%s made this the primary contact number.',
$this->renderAuthor());
}
public function validateTransactions($object, array $xactions) {
$errors = array();
foreach ($xactions as $xaction) {
$new_value = $xaction->getNewValue();
if (!$new_value) {
$errors[] = $this->newInvalidError(
pht(
'To choose a different primary contact number, make that '.
'number primary (instead of trying to demote this one).'),
$xaction);
continue;
}
if ($object->isDisabled()) {
$errors[] = $this->newInvalidError(
pht(
'You can not make a disabled number a primary contact number.'),
$xaction);
continue;
}
+
+ $mfa_error = $this->newContactNumberMFAError($object, $xaction);
+ if ($mfa_error) {
+ $errors[] = $mfa_error;
+ continue;
+ }
}
return $errors;
}
}
diff --git a/src/applications/auth/xaction/PhabricatorAuthContactNumberStatusTransaction.php b/src/applications/auth/xaction/PhabricatorAuthContactNumberStatusTransaction.php
index 305243ae15..5dab6fe8c0 100644
--- a/src/applications/auth/xaction/PhabricatorAuthContactNumberStatusTransaction.php
+++ b/src/applications/auth/xaction/PhabricatorAuthContactNumberStatusTransaction.php
@@ -1,59 +1,65 @@
<?php
final class PhabricatorAuthContactNumberStatusTransaction
extends PhabricatorAuthContactNumberTransactionType {
const TRANSACTIONTYPE = 'status';
public function generateOldValue($object) {
return $object->getStatus();
}
public function applyInternalEffects($object, $value) {
$object->setStatus($value);
}
public function getTitle() {
$new = $this->getNewValue();
if ($new === PhabricatorAuthContactNumber::STATUS_DISABLED) {
return pht(
'%s disabled this contact number.',
$this->renderAuthor());
} else {
return pht(
'%s enabled this contact number.',
$this->renderAuthor());
}
}
public function validateTransactions($object, array $xactions) {
$errors = array();
$map = PhabricatorAuthContactNumber::getStatusNameMap();
foreach ($xactions as $xaction) {
$new_value = $xaction->getNewValue();
if (!isset($map[$new_value])) {
$errors[] = $this->newInvalidError(
pht(
'Status ("%s") is not a valid contact number status. Valid '.
'status constants are: %s.',
$new_value,
implode(', ', array_keys($map))),
$xaction);
continue;
}
+ $mfa_error = $this->newContactNumberMFAError($object, $xaction);
+ if ($mfa_error) {
+ $errors[] = $mfa_error;
+ continue;
+ }
+
// NOTE: Enabling a contact number may cause us to collide with another
// active contact number. However, there might also be a transaction in
// this group that changes the number itself. Since we can't easily
// predict if we'll collide or not, just let the duplicate key logic
// handle it when we do.
}
return $errors;
}
}
diff --git a/src/applications/auth/xaction/PhabricatorAuthContactNumberTransactionType.php b/src/applications/auth/xaction/PhabricatorAuthContactNumberTransactionType.php
index c32fbe6a30..a74c78d4c4 100644
--- a/src/applications/auth/xaction/PhabricatorAuthContactNumberTransactionType.php
+++ b/src/applications/auth/xaction/PhabricatorAuthContactNumberTransactionType.php
@@ -1,4 +1,72 @@
<?php
abstract class PhabricatorAuthContactNumberTransactionType
- extends PhabricatorModularTransactionType {}
+ extends PhabricatorModularTransactionType {
+
+ protected function newContactNumberMFAError($object, $xaction) {
+ // If a contact number is attached to a user and that user has SMS MFA
+ // configured, don't let the user modify their primary contact number or
+ // make another contact number into their primary number.
+
+ $primary_type =
+ PhabricatorAuthContactNumberPrimaryTransaction::TRANSACTIONTYPE;
+
+ if ($xaction->getTransactionType() === $primary_type) {
+ // We're trying to make a non-primary number into the primary number,
+ // so do MFA checks.
+ $is_primary = false;
+ } else if ($object->getIsPrimary()) {
+ // We're editing the primary number, so do MFA checks.
+ $is_primary = true;
+ } else {
+ // Editing a non-primary number and not making it primary, so this is
+ // fine.
+ return null;
+ }
+
+ $target_phid = $object->getObjectPHID();
+ $omnipotent = PhabricatorUser::getOmnipotentUser();
+
+ $user_configs = id(new PhabricatorAuthFactorConfigQuery())
+ ->setViewer($omnipotent)
+ ->withUserPHIDs(array($target_phid))
+ ->execute();
+
+ $problem_configs = array();
+ foreach ($user_configs as $config) {
+ $provider = $config->getFactorProvider();
+ $factor = $provider->getFactor();
+
+ if ($factor->isContactNumberFactor()) {
+ $problem_configs[] = $config;
+ }
+ }
+
+ if (!$problem_configs) {
+ return null;
+ }
+
+ $problem_config = head($problem_configs);
+
+ if ($is_primary) {
+ return $this->newInvalidError(
+ pht(
+ 'You currently have multi-factor authentication ("%s") which '.
+ 'depends on your primary contact number. You must remove this '.
+ 'authentication factor before you can modify or disable your '.
+ 'primary contact number.',
+ $problem_config->getFactorName()),
+ $xaction);
+ } else {
+ return $this->newInvalidError(
+ pht(
+ 'You currently have multi-factor authentication ("%s") which '.
+ 'depends on your primary contact number. You must remove this '.
+ 'authentication factor before you can designate a new primary '.
+ 'contact number.',
+ $problem_config->getFactorName()),
+ $xaction);
+ }
+ }
+
+}

File Metadata

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

Event Timeline