diff --git a/resources/sql/autopatches/20180120.auth.01.password.sql b/resources/sql/autopatches/20180120.auth.01.password.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20180120.auth.01.password.sql @@ -0,0 +1,10 @@ +CREATE TABLE {$NAMESPACE}_auth.auth_password ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + objectPHID VARBINARY(64) NOT NULL, + passwordType VARCHAR(64) NOT NULL COLLATE {$COLLATE_TEXT}, + passwordHash VARCHAR(128) NOT NULL COLLATE {$COLLATE_TEXT}, + isRevoked BOOL NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20180120.auth.02.passwordxaction.sql b/resources/sql/autopatches/20180120.auth.02.passwordxaction.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20180120.auth.02.passwordxaction.sql @@ -0,0 +1,19 @@ +CREATE TABLE {$NAMESPACE}_auth.auth_passwordtransaction ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + authorPHID VARBINARY(64) NOT NULL, + objectPHID VARBINARY(64) NOT NULL, + viewPolicy VARBINARY(64) NOT NULL, + editPolicy VARBINARY(64) NOT NULL, + commentPHID VARBINARY(64) DEFAULT NULL, + commentVersion INT UNSIGNED NOT NULL, + transactionType VARCHAR(32) COLLATE {$COLLATE_TEXT} NOT NULL, + oldValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + newValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + contentSource LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + metadata LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; 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 @@ -2088,7 +2088,16 @@ 'PhabricatorAuthOldOAuthRedirectController' => 'applications/auth/controller/PhabricatorAuthOldOAuthRedirectController.php', 'PhabricatorAuthOneTimeLoginController' => 'applications/auth/controller/PhabricatorAuthOneTimeLoginController.php', 'PhabricatorAuthOneTimeLoginTemporaryTokenType' => 'applications/auth/tokentype/PhabricatorAuthOneTimeLoginTemporaryTokenType.php', + 'PhabricatorAuthPassword' => 'applications/auth/storage/PhabricatorAuthPassword.php', + 'PhabricatorAuthPasswordEditor' => 'applications/auth/editor/PhabricatorAuthPasswordEditor.php', + 'PhabricatorAuthPasswordPHIDType' => 'applications/auth/phid/PhabricatorAuthPasswordPHIDType.php', + 'PhabricatorAuthPasswordQuery' => 'applications/auth/query/PhabricatorAuthPasswordQuery.php', 'PhabricatorAuthPasswordResetTemporaryTokenType' => 'applications/auth/tokentype/PhabricatorAuthPasswordResetTemporaryTokenType.php', + 'PhabricatorAuthPasswordRevokeTransaction' => 'applications/auth/xaction/PhabricatorAuthPasswordRevokeTransaction.php', + 'PhabricatorAuthPasswordRevoker' => 'applications/auth/revoker/PhabricatorAuthPasswordRevoker.php', + 'PhabricatorAuthPasswordTestCase' => 'applications/auth/__tests__/PhabricatorAuthPasswordTestCase.php', + 'PhabricatorAuthPasswordTransaction' => 'applications/auth/storage/PhabricatorAuthPasswordTransaction.php', + 'PhabricatorAuthPasswordTransactionType' => 'applications/auth/xaction/PhabricatorAuthPasswordTransactionType.php', 'PhabricatorAuthProvider' => 'applications/auth/provider/PhabricatorAuthProvider.php', 'PhabricatorAuthProviderConfig' => 'applications/auth/storage/PhabricatorAuthProviderConfig.php', 'PhabricatorAuthProviderConfigController' => 'applications/auth/controller/config/PhabricatorAuthProviderConfigController.php', @@ -3479,6 +3488,7 @@ 'PhabricatorPagerUIExample' => 'applications/uiexample/examples/PhabricatorPagerUIExample.php', 'PhabricatorPassphraseApplication' => 'applications/passphrase/application/PhabricatorPassphraseApplication.php', 'PhabricatorPasswordAuthProvider' => 'applications/auth/provider/PhabricatorPasswordAuthProvider.php', + 'PhabricatorPasswordDestructionEngineExtension' => 'applications/auth/extension/PhabricatorPasswordDestructionEngineExtension.php', 'PhabricatorPasswordHasher' => 'infrastructure/util/password/PhabricatorPasswordHasher.php', 'PhabricatorPasswordHasherTestCase' => 'infrastructure/util/password/__tests__/PhabricatorPasswordHasherTestCase.php', 'PhabricatorPasswordHasherUnavailableException' => 'infrastructure/util/password/PhabricatorPasswordHasherUnavailableException.php', @@ -7363,7 +7373,21 @@ 'PhabricatorAuthOldOAuthRedirectController' => 'PhabricatorAuthController', 'PhabricatorAuthOneTimeLoginController' => 'PhabricatorAuthController', 'PhabricatorAuthOneTimeLoginTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType', + 'PhabricatorAuthPassword' => array( + 'PhabricatorAuthDAO', + 'PhabricatorPolicyInterface', + 'PhabricatorDestructibleInterface', + 'PhabricatorApplicationTransactionInterface', + ), + 'PhabricatorAuthPasswordEditor' => 'PhabricatorApplicationTransactionEditor', + 'PhabricatorAuthPasswordPHIDType' => 'PhabricatorPHIDType', + 'PhabricatorAuthPasswordQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorAuthPasswordResetTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType', + 'PhabricatorAuthPasswordRevokeTransaction' => 'PhabricatorAuthPasswordTransactionType', + 'PhabricatorAuthPasswordRevoker' => 'PhabricatorAuthRevoker', + 'PhabricatorAuthPasswordTestCase' => 'PhabricatorTestCase', + 'PhabricatorAuthPasswordTransaction' => 'PhabricatorApplicationTransaction', + 'PhabricatorAuthPasswordTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorAuthProvider' => 'Phobject', 'PhabricatorAuthProviderConfig' => array( 'PhabricatorAuthDAO', @@ -8980,6 +9004,7 @@ 'PhabricatorPagerUIExample' => 'PhabricatorUIExample', 'PhabricatorPassphraseApplication' => 'PhabricatorApplication', 'PhabricatorPasswordAuthProvider' => 'PhabricatorAuthProvider', + 'PhabricatorPasswordDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension', 'PhabricatorPasswordHasher' => 'Phobject', 'PhabricatorPasswordHasherTestCase' => 'PhabricatorTestCase', 'PhabricatorPasswordHasherUnavailableException' => 'Exception', diff --git a/src/applications/auth/__tests__/PhabricatorAuthPasswordTestCase.php b/src/applications/auth/__tests__/PhabricatorAuthPasswordTestCase.php new file mode 100644 --- /dev/null +++ b/src/applications/auth/__tests__/PhabricatorAuthPasswordTestCase.php @@ -0,0 +1,31 @@ + true, + ); + } + + public function testCompare() { + $password1 = new PhutilOpaqueEnvelope('hunter2'); + $password2 = new PhutilOpaqueEnvelope('hunter3'); + + $user = $this->generateNewTestUser(); + $type = PhabricatorAuthPassword::PASSWORD_TYPE_TEST; + + $pass = PhabricatorAuthPassword::initializeNewPassword($user, $type) + ->setPassword($password1, $user) + ->save(); + + $this->assertTrue( + $pass->comparePassword($password1, $user), + pht('Good password should match.')); + + $this->assertFalse( + $pass->comparePassword($password2, $user), + pht('Bad password should not match.')); + } + +} diff --git a/src/applications/auth/editor/PhabricatorAuthPasswordEditor.php b/src/applications/auth/editor/PhabricatorAuthPasswordEditor.php new file mode 100644 --- /dev/null +++ b/src/applications/auth/editor/PhabricatorAuthPasswordEditor.php @@ -0,0 +1,22 @@ +getViewer(); + $object_phid = $object->getPHID(); + + $passwords = id(new PhabricatorAuthPasswordQuery()) + ->setViewer($viewer) + ->withObjectPHIDs(array($object_phid)) + ->execute(); + + foreach ($passwords as $password) { + $engine->destroyObject($password); + } + } + +} diff --git a/src/applications/auth/phid/PhabricatorAuthPasswordPHIDType.php b/src/applications/auth/phid/PhabricatorAuthPasswordPHIDType.php new file mode 100644 --- /dev/null +++ b/src/applications/auth/phid/PhabricatorAuthPasswordPHIDType.php @@ -0,0 +1,36 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $password = $objects[$phid]; + } + } + +} diff --git a/src/applications/auth/query/PhabricatorAuthPasswordQuery.php b/src/applications/auth/query/PhabricatorAuthPasswordQuery.php new file mode 100644 --- /dev/null +++ b/src/applications/auth/query/PhabricatorAuthPasswordQuery.php @@ -0,0 +1,114 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withObjectPHIDs(array $object_phids) { + $this->objectPHIDs = $object_phids; + return $this; + } + + public function withPasswordTypes(array $types) { + $this->passwordTypes = $types; + return $this; + } + + public function withIsRevoked($is_revoked) { + $this->isRevoked = $is_revoked; + return $this; + } + + public function newResultObject() { + return new PhabricatorAuthPassword(); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn, + 'phid IN (%Ls)', + $this->phids); + } + + if ($this->objectPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'objectPHID IN (%Ls)', + $this->objectPHIDs); + } + + if ($this->passwordTypes !== null) { + $where[] = qsprintf( + $conn, + 'passwordType IN (%Ls)', + $this->passwordTypes); + } + + if ($this->isRevoked !== null) { + $where[] = qsprintf( + $conn, + 'isRevoked = %d', + (int)$this->isRevoked); + } + + return $where; + } + + protected function willFilterPage(array $passwords) { + $object_phids = mpull($passwords, 'getObjectPHID'); + + $objects = id(new PhabricatorObjectQuery()) + ->setViewer($this->getViewer()) + ->setParentQuery($this) + ->withPHIDs($object_phids) + ->execute(); + $objects = mpull($objects, null, 'getPHID'); + + foreach ($passwords as $key => $password) { + $object = idx($objects, $password->getObjectPHID()); + if (!$object) { + unset($passwords[$key]); + $this->didRejectResult($password); + continue; + } + + $password->attachObject($object); + } + + return $passwords; + } + + public function getQueryApplicationClass() { + return 'PhabricatorAuthApplication'; + } + +} diff --git a/src/applications/auth/revoker/PhabricatorAuthSSHRevoker.php b/src/applications/auth/revoker/PhabricatorAuthPasswordRevoker.php copy from src/applications/auth/revoker/PhabricatorAuthSSHRevoker.php copy to src/applications/auth/revoker/PhabricatorAuthPasswordRevoker.php --- a/src/applications/auth/revoker/PhabricatorAuthSSHRevoker.php +++ b/src/applications/auth/revoker/PhabricatorAuthPasswordRevoker.php @@ -1,52 +1,52 @@ revokeWithQuery($query); } public function revokeCredentialsFrom($object) { - $query = id(new PhabricatorAuthSSHKeyQuery()) + $query = id(new PhabricatorAuthPasswordQuery()) ->withObjectPHIDs(array($object->getPHID())); - return $this->revokeWithQuery($query); } private function revokeWithQuery(PhabricatorAuthSSHKeyQuery $query) { $viewer = $this->getViewer(); - // We're only going to revoke keys which have not already been revoked. - - $ssh_keys = $query + $passwords = $query ->setViewer($viewer) - ->withIsActive(true) + ->withIsRevoked(false) ->execute(); $content_source = PhabricatorContentSource::newForSource( PhabricatorDaemonContentSource::SOURCECONST); + $revoke_type = PhabricatorAuthPasswordRevokeTransaction::TRANSACTIONTYPE; + $auth_phid = id(new PhabricatorAuthApplication())->getPHID(); - foreach ($ssh_keys as $ssh_key) { + foreach ($passwords as $password) { $xactions = array(); - $xactions[] = $ssh_key->getApplicationTransactionTemplate() - ->setTransactionType(PhabricatorAuthSSHKeyTransaction::TYPE_DEACTIVATE) - ->setNewValue(1); - $editor = id(new PhabricatorAuthSSHKeyEditor()) + $xactions[] = $password->getApplicationTransactionTemplate() + ->setTransactionType($revoke_type) + ->setNewValue(true); + + $editor = $password->getApplicationTransactionEditor() ->setActor($viewer) ->setActingAsPHID($auth_phid) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->setContentSource($content_source) - ->applyTransactions($ssh_key, $xactions); + ->applyTransactions($password, $xactions); } - return count($ssh_keys); + return count($passwords); } } diff --git a/src/applications/auth/revoker/PhabricatorAuthSSHRevoker.php b/src/applications/auth/revoker/PhabricatorAuthSSHRevoker.php --- a/src/applications/auth/revoker/PhabricatorAuthSSHRevoker.php +++ b/src/applications/auth/revoker/PhabricatorAuthSSHRevoker.php @@ -37,7 +37,7 @@ ->setTransactionType(PhabricatorAuthSSHKeyTransaction::TYPE_DEACTIVATE) ->setNewValue(1); - $editor = id(new PhabricatorAuthSSHKeyEditor()) + $editor = $ssh_key->getApplicationTransactionEditor() ->setActor($viewer) ->setActingAsPHID($auth_phid) ->setContinueOnNoEffect(true) diff --git a/src/applications/auth/storage/PhabricatorAuthPassword.php b/src/applications/auth/storage/PhabricatorAuthPassword.php new file mode 100644 --- /dev/null +++ b/src/applications/auth/storage/PhabricatorAuthPassword.php @@ -0,0 +1,167 @@ +setObjectPHID($object->getPHID()) + ->attachObject($object) + ->setPasswordType($type) + ->setIsRevoked(0); + } + + protected function getConfiguration() { + return array( + self::CONFIG_AUX_PHID => true, + self::CONFIG_COLUMN_SCHEMA => array( + 'passwordType' => 'text64', + 'passwordHash' => 'text128', + 'isRevoked' => 'bool', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_role' => array( + 'columns' => array('objectPHID', 'passwordType'), + ), + ), + ) + parent::getConfiguration(); + } + + public function getPHIDType() { + return PhabricatorAuthPasswordPHIDType::TYPECONST; + } + + public function getObject() { + return $this->assertAttached($this->object); + } + + public function attachObject($object) { + $this->object = $object; + return $this; + } + + public function setPassword( + PhutilOpaqueEnvelope $password, + PhabricatorUser $object) { + + $hasher = PhabricatorPasswordHasher::getBestHasher(); + + $digest = $this->digestPassword($password, $object); + $hash = $hasher->getPasswordHashForStorage($digest); + $raw_hash = $hash->openEnvelope(); + + return $this->setPasswordHash($raw_hash); + } + + public function comparePassword( + PhutilOpaqueEnvelope $password, + PhabricatorUser $object) { + + $digest = $this->digestPassword($password, $object); + $raw_hash = $this->getPasswordHash(); + $hash = new PhutilOpaqueEnvelope($raw_hash); + + return PhabricatorPasswordHasher::comparePassword($digest, $hash); + } + + private function digestPassword( + PhutilOpaqueEnvelope $password, + PhabricatorUser $object) { + + $object_phid = $object->getPHID(); + + if ($this->getObjectPHID() !== $object->getPHID()) { + throw new Exception( + pht( + 'This password is associated with a an object PHID ("%s") for '. + 'a different object than the provided one ("%s").', + $this->getObjectPHID(), + $object->getPHID())); + } + + $raw_input = PhabricatorHash::digestPassword($password, $object_phid); + + return new PhutilOpaqueEnvelope($raw_input); + } + + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + ); + } + + public function getPolicy($capability) { + return PhabricatorPolicies::getMostOpenPolicy(); + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return false; + } + + +/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ + + + public function getExtendedPolicy($capability, PhabricatorUser $viewer) { + return array( + array($this->getObject(), PhabricatorPolicyCapability::CAN_VIEW), + ); + } + + +/* -( PhabricatorDestructibleInterface )----------------------------------- */ + + + public function destroyObjectPermanently( + PhabricatorDestructionEngine $engine) { + $this->delete(); + } + + +/* -( PhabricatorApplicationTransactionInterface )------------------------- */ + + + public function getApplicationTransactionEditor() { + return new PhabricatorAuthPasswordEditor(); + } + + public function getApplicationTransactionObject() { + return $this; + } + + public function getApplicationTransactionTemplate() { + return new PhabricatorAuthPasswordTransaction(); + } + + public function willRenderTimeline( + PhabricatorApplicationTransactionView $timeline, + AphrontRequest $request) { + + return $timeline; + } + + +} diff --git a/src/applications/auth/storage/PhabricatorAuthPasswordTransaction.php b/src/applications/auth/storage/PhabricatorAuthPasswordTransaction.php new file mode 100644 --- /dev/null +++ b/src/applications/auth/storage/PhabricatorAuthPasswordTransaction.php @@ -0,0 +1,21 @@ +getIsRevoked(); + } + + public function generateNewValue($object, $value) { + return (bool)$value; + } + + public function applyInternalEffects($object, $value) { + $object->setIsRevoked((int)$value); + } + + public function getTitle() { + if ($this->getNewValue()) { + return pht( + '%s revoked this password.', + $this->renderAuthor()); + } else { + return pht( + '%s reinstated this password.', + $this->renderAuthor()); + } + } + +} diff --git a/src/applications/auth/xaction/PhabricatorAuthPasswordTransactionType.php b/src/applications/auth/xaction/PhabricatorAuthPasswordTransactionType.php new file mode 100644 --- /dev/null +++ b/src/applications/auth/xaction/PhabricatorAuthPasswordTransactionType.php @@ -0,0 +1,4 @@ +