1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2025-02-26 05:29:06 +01:00
phorge-phorge/src/applications/people/storage/PhabricatorUser.php

1584 lines
43 KiB
PHP
Raw Normal View History

2011-01-23 18:09:16 -08:00
<?php
/**
* @task availability Availability
* @task image-cache Profile Image Cache
* @task factors Multi-Factor Authentication
* @task handles Managing Handles
* @task settings Settings
Provide a general-purpose, modular user cache for settings and other similar data Summary: Ref T4103. Currently, we issue a `SELECT * FROM user_preferences ... WHERE userPHID = ...` on every page to load the viewer's settings. There are several other questionable data accesses on every page too, most of which could benefit from improved caching strategies (see T4103#178122). This query will soon get more expensive, since it may need to load several objects (e.g., the user's settings and their "role profile" settings). Although we could put that data on the User and do both in one query, it's nicer to put it on the Preferences object ("This inherits from profile X") which means we need to do several queries. Rather than paying a greater price, we can cheat this stuff into the existing query where we load the user's session by providing a user cache table and doing some JOIN magic. This lets us issue one query and try to get cache hits on a bunch of caches cheaply (well, we'll be in trouble at the MySQL JOIN limit of 61 tables, but have some headroom). For now, just get it working: - Add the table. - Try to get user settings "for free" when we load the session. - If we miss, fill user settings into the cache on-demand. - We only use this in one place (DarkConsole) for now. I'll use it more widely in the next diff. Test Plan: - Loaded page as logged-in user. - Loaded page as logged-out user. - Examined session query to see cache joins. - Changed settings, saw database cache fill. - Toggled DarkConsole on and off. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4103 Differential Revision: https://secure.phabricator.com/D16001
2016-06-01 12:14:39 -07:00
* @task cache User Cache
*/
final class PhabricatorUser
extends PhabricatorUserDAO
implements
PhutilPerson,
PhabricatorPolicyInterface,
PhabricatorCustomFieldInterface,
PhabricatorDestructibleInterface,
PhabricatorSSHPublicKeyInterface,
PhabricatorFlaggableInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorFulltextInterface,
PhabricatorConduitResultInterface {
2011-01-23 18:09:16 -08:00
2011-02-05 22:36:21 -08:00
const SESSION_TABLE = 'phabricator_session';
const NAMETOKEN_TABLE = 'user_nametoken';
const MAXIMUM_USERNAME_LENGTH = 64;
2011-02-05 22:36:21 -08:00
2011-01-23 18:09:16 -08:00
protected $userName;
protected $realName;
2011-01-26 13:21:12 -08:00
protected $passwordSalt;
protected $passwordHash;
2011-01-30 21:28:45 -08:00
protected $profileImagePHID;
protected $availabilityCache;
protected $availabilityCacheTTL;
2011-01-23 18:09:16 -08:00
2011-02-05 22:36:21 -08:00
protected $conduitCertificate;
2011-04-12 19:00:54 -07:00
protected $isSystemAgent = 0;
protected $isMailingList = 0;
protected $isAdmin = 0;
protected $isDisabled = 0;
Improve handling of email verification and "activated" accounts Summary: Small step forward which improves existing stuff or lays groudwork for future stuff: - Currently, to check for email verification, we have to single-query the email address on every page. Instead, denoramlize it into the user object. - Migrate all the existing users. - When the user verifies an email, mark them as `isEmailVerified` if the email is their primary email. - Just make the checks look at the `isEmailVerified` field. - Add a new check, `isUserActivated()`, to cover email-verified plus disabled. Currently, a non-verified-but-not-disabled user could theoretically use Conduit over SSH, if anyone deployed it. Tighten that up. - Add an `isApproved` flag, which is always true for now. In a future diff, I want to add a default-on admin approval queue for new accounts, to prevent configuration mistakes. The way it will work is: - When the queue is enabled, registering users are created with `isApproved = false`. - Admins are sent an email, "[Phabricator] New User Approval (alincoln)", telling them that a new user is waiting for approval. - They go to the web UI and approve the user. - Manually-created accounts are auto-approved. - The email will have instructions for disabling the queue. I think this queue will be helpful for new installs and give them peace of mind, and when you go to disable it we have a better opportunity to warn you about exactly what that means. Generally, I want to improve the default safety of registration, since if you just blindly coast through the path of least resistance right now your install ends up pretty open, and realistically few installs are on VPNs. Test Plan: - Ran migration, verified `isEmailVerified` populated correctly. - Created a new user, checked DB for verified (not verified). - Verified, checked DB (now verified). - Used Conduit, People, Diffusion. Reviewers: btrahan Reviewed By: btrahan CC: chad, aran Differential Revision: https://secure.phabricator.com/D7572
2013-11-12 14:37:04 -08:00
protected $isEmailVerified = 0;
protected $isApproved = 0;
protected $isEnrolledInMultiFactor = 0;
2011-04-12 18:19:09 -07:00
protected $accountSecret;
private $profile = null;
private $availability = self::ATTACHABLE;
private $preferences = null;
private $omnipotent = false;
private $customFields = self::ATTACHABLE;
private $badgePHIDs = self::ATTACHABLE;
private $alternateCSRFString = self::ATTACHABLE;
private $session = self::ATTACHABLE;
Provide a general-purpose, modular user cache for settings and other similar data Summary: Ref T4103. Currently, we issue a `SELECT * FROM user_preferences ... WHERE userPHID = ...` on every page to load the viewer's settings. There are several other questionable data accesses on every page too, most of which could benefit from improved caching strategies (see T4103#178122). This query will soon get more expensive, since it may need to load several objects (e.g., the user's settings and their "role profile" settings). Although we could put that data on the User and do both in one query, it's nicer to put it on the Preferences object ("This inherits from profile X") which means we need to do several queries. Rather than paying a greater price, we can cheat this stuff into the existing query where we load the user's session by providing a user cache table and doing some JOIN magic. This lets us issue one query and try to get cache hits on a bunch of caches cheaply (well, we'll be in trouble at the MySQL JOIN limit of 61 tables, but have some headroom). For now, just get it working: - Add the table. - Try to get user settings "for free" when we load the session. - If we miss, fill user settings into the cache on-demand. - We only use this in one place (DarkConsole) for now. I'll use it more widely in the next diff. Test Plan: - Loaded page as logged-in user. - Loaded page as logged-out user. - Examined session query to see cache joins. - Changed settings, saw database cache fill. - Toggled DarkConsole on and off. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4103 Differential Revision: https://secure.phabricator.com/D16001
2016-06-01 12:14:39 -07:00
private $rawCacheData = array();
private $usableCacheData = array();
Improve Phortune policy behavior Summary: Currently, PhortuneAccounts have a very open default policy to allow merchants to see and interact with them. This has the undesirable side effect of leaking their names in too many places, because all users are allowed to load the handles for the accounts. Although this information is not super sensitive, we shouldn't expose it. I went through about 5 really messy diffs trying to fix this. It's very complicated because there are a lot of objects and many of them are related to PhortuneAccounts, but PhortuneAccounts are not bound to a specific merchant. This lead to a lot of threading viewers and merchants all over the place through the call stack and some really sketchy diffs with OmnipotentUsers that weren't going anywhere good. This is the cleanest approach I came up with, by far: - Introduce the concept of an "Authority", which gives a user more powers as a viewer. For now, since we only have one use case, this is pretty open-ended. - When a viewer is acting as a merchant, grant them authority through the merchant. - Have Accounts check if the viewer is acting with merchant authority. This lets us easily implement the rule "merchants can see this stuff" without being too broad. Then update the Subscription view to respect Merchant Authority. I partially updated the Cart views to respect it. I'll finish this up in a separate diff, but this seemed like a good checkpoint that introduced the concept without too much extra baggage. This feels pretty good/clean to me, overall, even ignoring the series of horrible messes I made on my way here. Test Plan: - Verified I can see everything I need to as a merchant (modulo un-updated Cart UIs). - Verified I can see nothing when acting as a normal user. Reviewers: btrahan Reviewed By: btrahan Subscribers: epriestley Differential Revision: https://secure.phabricator.com/D11945
2015-03-03 10:38:25 -08:00
private $authorities = array();
private $handlePool;
private $csrfSalt;
Continue modernizing application access to user preferences Summary: Ref T4103. This is just incremental cleanup: - Add "internal" settings, which aren't editable via the UI. They can still do validation and run through the normal pathway. Move a couple settings to use this. - Remove `getPreference()` on `PhabricatorUser`, which was a sort of prototype version of `getUserSetting()`. - Make `getUserSetting()` validate setting values before returning them, to improve robustness if we change allowable values later. - Add a user setting cache, since reading user settings was getting fairly expensive on Calendar. - Improve performance of setting validation for timezone setting (don't require building/computing all timezone offsets). - Since we have the cache anyway, make the timezone override a little more general in its approach. - Move editor stuff to use `getUserSetting()`. Test Plan: - Changed search scopes. - Reconciled local and server timezone settings by ignoring and changing timezones. - Changed date/time settings, browsed Calendar, queried date ranges. - Verified editor links generate properly in Diffusion. - Browsed around with time/date settings looking at timestamps. - Grepped for `getPreference()`, nuked all the ones coming off `$user` or `$viewer` that I could find. - Changed accessiblity to high-contrast colors. - Ran all unit tests. - Grepped for removed constants. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4103 Differential Revision: https://secure.phabricator.com/D16015
2016-06-02 17:12:15 -07:00
private $settingCacheKeys = array();
private $settingCache = array();
private $allowInlineCacheGeneration;
private $conduitClusterToken = self::ATTACHABLE;
Improve Phortune policy behavior Summary: Currently, PhortuneAccounts have a very open default policy to allow merchants to see and interact with them. This has the undesirable side effect of leaking their names in too many places, because all users are allowed to load the handles for the accounts. Although this information is not super sensitive, we shouldn't expose it. I went through about 5 really messy diffs trying to fix this. It's very complicated because there are a lot of objects and many of them are related to PhortuneAccounts, but PhortuneAccounts are not bound to a specific merchant. This lead to a lot of threading viewers and merchants all over the place through the call stack and some really sketchy diffs with OmnipotentUsers that weren't going anywhere good. This is the cleanest approach I came up with, by far: - Introduce the concept of an "Authority", which gives a user more powers as a viewer. For now, since we only have one use case, this is pretty open-ended. - When a viewer is acting as a merchant, grant them authority through the merchant. - Have Accounts check if the viewer is acting with merchant authority. This lets us easily implement the rule "merchants can see this stuff" without being too broad. Then update the Subscription view to respect Merchant Authority. I partially updated the Cart views to respect it. I'll finish this up in a separate diff, but this seemed like a good checkpoint that introduced the concept without too much extra baggage. This feels pretty good/clean to me, overall, even ignoring the series of horrible messes I made on my way here. Test Plan: - Verified I can see everything I need to as a merchant (modulo un-updated Cart UIs). - Verified I can see nothing when acting as a normal user. Reviewers: btrahan Reviewed By: btrahan Subscribers: epriestley Differential Revision: https://secure.phabricator.com/D11945
2015-03-03 10:38:25 -08:00
protected function readField($field) {
switch ($field) {
// Make sure these return booleans.
case 'isAdmin':
return (bool)$this->isAdmin;
case 'isDisabled':
return (bool)$this->isDisabled;
case 'isSystemAgent':
return (bool)$this->isSystemAgent;
case 'isMailingList':
return (bool)$this->isMailingList;
Improve handling of email verification and "activated" accounts Summary: Small step forward which improves existing stuff or lays groudwork for future stuff: - Currently, to check for email verification, we have to single-query the email address on every page. Instead, denoramlize it into the user object. - Migrate all the existing users. - When the user verifies an email, mark them as `isEmailVerified` if the email is their primary email. - Just make the checks look at the `isEmailVerified` field. - Add a new check, `isUserActivated()`, to cover email-verified plus disabled. Currently, a non-verified-but-not-disabled user could theoretically use Conduit over SSH, if anyone deployed it. Tighten that up. - Add an `isApproved` flag, which is always true for now. In a future diff, I want to add a default-on admin approval queue for new accounts, to prevent configuration mistakes. The way it will work is: - When the queue is enabled, registering users are created with `isApproved = false`. - Admins are sent an email, "[Phabricator] New User Approval (alincoln)", telling them that a new user is waiting for approval. - They go to the web UI and approve the user. - Manually-created accounts are auto-approved. - The email will have instructions for disabling the queue. I think this queue will be helpful for new installs and give them peace of mind, and when you go to disable it we have a better opportunity to warn you about exactly what that means. Generally, I want to improve the default safety of registration, since if you just blindly coast through the path of least resistance right now your install ends up pretty open, and realistically few installs are on VPNs. Test Plan: - Ran migration, verified `isEmailVerified` populated correctly. - Created a new user, checked DB for verified (not verified). - Verified, checked DB (now verified). - Used Conduit, People, Diffusion. Reviewers: btrahan Reviewed By: btrahan CC: chad, aran Differential Revision: https://secure.phabricator.com/D7572
2013-11-12 14:37:04 -08:00
case 'isEmailVerified':
return (bool)$this->isEmailVerified;
case 'isApproved':
return (bool)$this->isApproved;
default:
return parent::readField($field);
}
2011-01-31 16:00:42 -08:00
}
2011-01-30 18:52:29 -08:00
Improve handling of email verification and "activated" accounts Summary: Small step forward which improves existing stuff or lays groudwork for future stuff: - Currently, to check for email verification, we have to single-query the email address on every page. Instead, denoramlize it into the user object. - Migrate all the existing users. - When the user verifies an email, mark them as `isEmailVerified` if the email is their primary email. - Just make the checks look at the `isEmailVerified` field. - Add a new check, `isUserActivated()`, to cover email-verified plus disabled. Currently, a non-verified-but-not-disabled user could theoretically use Conduit over SSH, if anyone deployed it. Tighten that up. - Add an `isApproved` flag, which is always true for now. In a future diff, I want to add a default-on admin approval queue for new accounts, to prevent configuration mistakes. The way it will work is: - When the queue is enabled, registering users are created with `isApproved = false`. - Admins are sent an email, "[Phabricator] New User Approval (alincoln)", telling them that a new user is waiting for approval. - They go to the web UI and approve the user. - Manually-created accounts are auto-approved. - The email will have instructions for disabling the queue. I think this queue will be helpful for new installs and give them peace of mind, and when you go to disable it we have a better opportunity to warn you about exactly what that means. Generally, I want to improve the default safety of registration, since if you just blindly coast through the path of least resistance right now your install ends up pretty open, and realistically few installs are on VPNs. Test Plan: - Ran migration, verified `isEmailVerified` populated correctly. - Created a new user, checked DB for verified (not verified). - Verified, checked DB (now verified). - Used Conduit, People, Diffusion. Reviewers: btrahan Reviewed By: btrahan CC: chad, aran Differential Revision: https://secure.phabricator.com/D7572
2013-11-12 14:37:04 -08:00
/**
* Is this a live account which has passed required approvals? Returns true
* if this is an enabled, verified (if required), approved (if required)
* account, and false otherwise.
*
* @return bool True if this is a standard, usable account.
*/
public function isUserActivated() {
if (!$this->isLoggedIn()) {
return false;
}
if ($this->isOmnipotent()) {
return true;
}
Improve handling of email verification and "activated" accounts Summary: Small step forward which improves existing stuff or lays groudwork for future stuff: - Currently, to check for email verification, we have to single-query the email address on every page. Instead, denoramlize it into the user object. - Migrate all the existing users. - When the user verifies an email, mark them as `isEmailVerified` if the email is their primary email. - Just make the checks look at the `isEmailVerified` field. - Add a new check, `isUserActivated()`, to cover email-verified plus disabled. Currently, a non-verified-but-not-disabled user could theoretically use Conduit over SSH, if anyone deployed it. Tighten that up. - Add an `isApproved` flag, which is always true for now. In a future diff, I want to add a default-on admin approval queue for new accounts, to prevent configuration mistakes. The way it will work is: - When the queue is enabled, registering users are created with `isApproved = false`. - Admins are sent an email, "[Phabricator] New User Approval (alincoln)", telling them that a new user is waiting for approval. - They go to the web UI and approve the user. - Manually-created accounts are auto-approved. - The email will have instructions for disabling the queue. I think this queue will be helpful for new installs and give them peace of mind, and when you go to disable it we have a better opportunity to warn you about exactly what that means. Generally, I want to improve the default safety of registration, since if you just blindly coast through the path of least resistance right now your install ends up pretty open, and realistically few installs are on VPNs. Test Plan: - Ran migration, verified `isEmailVerified` populated correctly. - Created a new user, checked DB for verified (not verified). - Verified, checked DB (now verified). - Used Conduit, People, Diffusion. Reviewers: btrahan Reviewed By: btrahan CC: chad, aran Differential Revision: https://secure.phabricator.com/D7572
2013-11-12 14:37:04 -08:00
if ($this->getIsDisabled()) {
return false;
}
if (!$this->getIsApproved()) {
return false;
}
if (PhabricatorUserEmail::isEmailVerificationRequired()) {
if (!$this->getIsEmailVerified()) {
return false;
}
}
return true;
}
2017-02-17 07:18:44 -08:00
/**
* Is this a user who we can reasonably expect to respond to requests?
*
* This is used to provide a grey "disabled/unresponsive" dot cue when
* rendering handles and tags, so it isn't a surprise if you get ignored
* when you ask things of users who will not receive notifications or could
* not respond to them (because they are disabled, unapproved, do not have
* verified email addresses, etc).
*
* @return bool True if this user can receive and respond to requests from
* other humans.
*/
public function isResponsive() {
if (!$this->isUserActivated()) {
return false;
}
if (!$this->getIsEmailVerified()) {
return false;
}
return true;
}
public function canEstablishWebSessions() {
if ($this->getIsMailingList()) {
return false;
}
if ($this->getIsSystemAgent()) {
return false;
}
return true;
}
public function canEstablishAPISessions() {
if ($this->getIsDisabled()) {
return false;
}
// Intracluster requests are permitted even if the user is logged out:
// in particular, public users are allowed to issue intracluster requests
// when browsing Diffusion.
if (PhabricatorEnv::isClusterRemoteAddress()) {
if (!$this->isLoggedIn()) {
return true;
}
}
if (!$this->isUserActivated()) {
return false;
}
if ($this->getIsMailingList()) {
return false;
}
return true;
}
public function canEstablishSSHSessions() {
if (!$this->isUserActivated()) {
return false;
}
if ($this->getIsMailingList()) {
return false;
}
return true;
}
2014-04-03 18:43:18 -07:00
/**
* Returns `true` if this is a standard user who is logged in. Returns `false`
* for logged out, anonymous, or external users.
*
* @return bool `true` if the user is a standard user who is logged in with
* a normal session.
*/
public function getIsStandardUser() {
$type_user = PhabricatorPeopleUserPHIDType::TYPECONST;
2014-04-03 18:43:18 -07:00
return $this->getPHID() && (phid_get_type($this->getPHID()) == $type_user);
}
protected function getConfiguration() {
2011-01-23 18:09:16 -08:00
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'userName' => 'sort64',
'realName' => 'text128',
'passwordSalt' => 'text32?',
'passwordHash' => 'text128?',
'profileImagePHID' => 'phid?',
'conduitCertificate' => 'text255',
'isSystemAgent' => 'bool',
'isMailingList' => 'bool',
'isDisabled' => 'bool',
'isAdmin' => 'bool',
'isEmailVerified' => 'uint32',
'isApproved' => 'uint32',
'accountSecret' => 'bytes64',
'isEnrolledInMultiFactor' => 'bool',
'availabilityCache' => 'text255?',
'availabilityCacheTTL' => 'uint32?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'userName' => array(
'columns' => array('userName'),
'unique' => true,
),
'realName' => array(
'columns' => array('realName'),
),
'key_approved' => array(
'columns' => array('isApproved'),
),
),
self::CONFIG_NO_MUTATE => array(
'availabilityCache' => true,
'availabilityCacheTTL' => true,
),
2011-01-23 18:09:16 -08:00
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPeopleUserPHIDType::TYPECONST);
2011-01-23 18:09:16 -08:00
}
public function setPassword(PhutilOpaqueEnvelope $envelope) {
Mask typed passwords as they are entered into 'accountadmin' Summary: Currently, we echo the password as the user types it. This turns out to be a bit of an issue in over-the-shoulder installs. Instead, disable tty echo while the user is typing their password so nothing is shown (like how 'sudo' works). Also show a better error message if the user chooses a duplicate email; without testing for this we just throw a duplicate key exception when saving, which isn't easy to understand. The other duplicate key exception is duplicate username, which is impossible (the script updates rather than creating in this case). There's currently a bug where creating a user and setting their password at the same time doesn't work. This is because we hash the PHID into the password hash, but it's empty if the user hasn't been persisted yet. Make sure the user is persisted before setting their password. Finally, fix an issue where $original would have the new username set, creating a somewhat confusing summary at the end. I'm also going to improve the password behavior/explanation here once I add welcome emails ("Hi Joe, epriestley created an account for you on Phabricator, click here to login..."). Test Plan: - Typed a password and didn't have it echoed. I also tested this on Ubuntu without encountering problems. - Chose a duplicate email, got a useful error message instead of the exception I'd encountered earlier. - Created a new user with a password in one pass and logged in as that user, this worked properly. - Verified summary table does not contain username for new users. Reviewed By: jungejason Reviewers: jungejason, tuomaspelkonen, aran CC: moskov, jr, aran, jungejason Differential Revision: 358
2011-05-28 07:17:42 -07:00
if (!$this->getPHID()) {
throw new Exception(
pht(
'You can not set a password for an unsaved user because their PHID '.
'is a salt component in the password hash.'));
Mask typed passwords as they are entered into 'accountadmin' Summary: Currently, we echo the password as the user types it. This turns out to be a bit of an issue in over-the-shoulder installs. Instead, disable tty echo while the user is typing their password so nothing is shown (like how 'sudo' works). Also show a better error message if the user chooses a duplicate email; without testing for this we just throw a duplicate key exception when saving, which isn't easy to understand. The other duplicate key exception is duplicate username, which is impossible (the script updates rather than creating in this case). There's currently a bug where creating a user and setting their password at the same time doesn't work. This is because we hash the PHID into the password hash, but it's empty if the user hasn't been persisted yet. Make sure the user is persisted before setting their password. Finally, fix an issue where $original would have the new username set, creating a somewhat confusing summary at the end. I'm also going to improve the password behavior/explanation here once I add welcome emails ("Hi Joe, epriestley created an account for you on Phabricator, click here to login..."). Test Plan: - Typed a password and didn't have it echoed. I also tested this on Ubuntu without encountering problems. - Chose a duplicate email, got a useful error message instead of the exception I'd encountered earlier. - Created a new user with a password in one pass and logged in as that user, this worked properly. - Verified summary table does not contain username for new users. Reviewed By: jungejason Reviewers: jungejason, tuomaspelkonen, aran CC: moskov, jr, aran, jungejason Differential Revision: 358
2011-05-28 07:17:42 -07:00
}
if (!strlen($envelope->openEnvelope())) {
$this->setPasswordHash('');
} else {
$this->setPasswordSalt(md5(Filesystem::readRandomBytes(32)));
$hash = $this->hashPassword($envelope);
$this->setPasswordHash($hash->openEnvelope());
}
2011-01-26 13:21:12 -08:00
return $this;
}
public function getMonogram() {
return '@'.$this->getUsername();
}
public function isLoggedIn() {
return !($this->getPHID() === null);
}
2011-02-05 22:36:21 -08:00
public function save() {
if (!$this->getConduitCertificate()) {
$this->setConduitCertificate($this->generateConduitCertificate());
2011-02-05 22:36:21 -08:00
}
if (!strlen($this->getAccountSecret())) {
$this->setAccountSecret(Filesystem::readRandomCharacters(64));
}
$result = parent::save();
if ($this->profile) {
$this->profile->save();
}
$this->updateNameTokens();
Improve Search architecture Summary: The search indexing API has several problems right now: - Always runs in-process. - It would be nice to push this into the task queue for performance. However, the API currently passses an object all the way through (and some indexers depend on preloaded object attributes), so it can't be dumped into the task queue at any stage since we can't serialize it. - Being able to use the task queue will also make rebuilding indexes faster. - Instead, make the API phid-oriented. - No uniform indexing API. - Each "Editor" currently calls SomeCustomIndexer::indexThing(). This won't work with AbstractTransactions. The API is also just weird. - Instead, provide a uniform API. - No uniform CLI. - We have `scripts/search/reindex_everything.php`, but it doesn't actually index everything. Each new document type needs to be separately added to it, leading to stuff like D3839. Third-party applications can't provide indexers. - Instead, let indexers expose documents for indexing. - Not application-oriented. - All the indexers live in search/ right now, which isn't the right organization in an application-orietned view of the world. - Instead, move indexers to applications and load them with SymbolLoader. Test Plan: - `bin/search index` - Indexed one revision, one task. - Indexed `--type TASK`, `--type DREV`, etc., for all types. - Indexed `--all`. - Added the word "saboteur" to a revision, task, wiki page, and question and then searched for it. - Creating users is a pain; searched for a user after indexing. - Creating commits is a pain; searched for a commit after indexing. - Mocks aren't currently loadable in the result view, so their indexing is moot. Reviewers: btrahan, vrana Reviewed By: btrahan CC: 20after4, aran Maniphest Tasks: T1991, T2104 Differential Revision: https://secure.phabricator.com/D4261
2012-12-21 14:21:31 -08:00
PhabricatorSearchWorker::queueDocumentForIndexing($this->getPHID());
return $result;
2011-02-05 22:36:21 -08:00
}
public function attachSession(PhabricatorAuthSession $session) {
$this->session = $session;
return $this;
}
public function getSession() {
return $this->assertAttached($this->session);
}
public function hasSession() {
return ($this->session !== self::ATTACHABLE);
}
2011-02-05 22:36:21 -08:00
private function generateConduitCertificate() {
return Filesystem::readRandomCharacters(255);
2011-02-05 22:36:21 -08:00
}
public function comparePassword(PhutilOpaqueEnvelope $envelope) {
if (!strlen($envelope->openEnvelope())) {
return false;
}
if (!strlen($this->getPasswordHash())) {
return false;
}
return PhabricatorPasswordHasher::comparePassword(
$this->getPasswordHashInput($envelope),
new PhutilOpaqueEnvelope($this->getPasswordHash()));
2011-01-26 13:21:12 -08:00
}
private function getPasswordHashInput(PhutilOpaqueEnvelope $password) {
$input =
$this->getUsername().
$password->openEnvelope().
$this->getPHID().
$this->getPasswordSalt();
return new PhutilOpaqueEnvelope($input);
}
private function hashPassword(PhutilOpaqueEnvelope $password) {
$hasher = PhabricatorPasswordHasher::getBestHasher();
$input_envelope = $this->getPasswordHashInput($password);
return $hasher->getPasswordHashForStorage($input_envelope);
2011-01-26 13:21:12 -08:00
}
const CSRF_CYCLE_FREQUENCY = 3600;
const CSRF_SALT_LENGTH = 8;
const CSRF_TOKEN_LENGTH = 16;
const CSRF_BREACH_PREFIX = 'B@';
const EMAIL_CYCLE_FREQUENCY = 86400;
const EMAIL_TOKEN_LENGTH = 24;
private function getRawCSRFToken($offset = 0) {
return $this->generateToken(
time() + (self::CSRF_CYCLE_FREQUENCY * $offset),
self::CSRF_CYCLE_FREQUENCY,
PhabricatorEnv::getEnvConfig('phabricator.csrf-key'),
self::CSRF_TOKEN_LENGTH);
2011-01-30 18:52:29 -08:00
}
public function getCSRFToken() {
Don't try to generate a CSRF token for the omnipotent user Summary: We can end up here with a stack trace like this, while rendering an embedded Slowvote trying to publish a Feed story: ``` Daemon 43450 STDE [Mon, 08 Jun 2015 22:49:57 +0000] [2015-06-08 22:49:57] EXCEPTION: (PhutilProxyException) Error while executing Task ID 830591. {>} (PhabricatorDataNotAttachedException) Attempting to access attached data on PhabricatorUser (via getAlternateCSRFString()), but the data is not actually attached. Before accessing attachable data on an object, you must load and attach it. Daemon 43450 STDE [Mon, 08 Jun 2015 22:49:57 +0000] Daemon 43450 STDE [Mon, 08 Jun 2015 22:49:57 +0000] Data is normally attached by calling the corresponding needX() method on the Query class when the object is loaded. You can also call the corresponding attachX() method explicitly. at [<phabricator>/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php:166] Daemon 43450 STDE [Mon, 08 Jun 2015 22:49:57 +0000] arcanist(head=master, ref.master=7d15b85a1bc0), phabricator(head=master, ref.master=929f5f22acef), phutil(head=master, ref.master=92882eb9404d) Daemon 43450 STDE [Mon, 08 Jun 2015 22:49:57 +0000] #0 <#2> PhabricatorLiskDAO::assertAttached(string) called at [<phabricator>/src/applications/people/storage/PhabricatorUser.php:556] Daemon 43450 STDE [Mon, 08 Jun 2015 22:49:57 +0000] #1 <#2> PhabricatorUser::getAlternateCSRFString() called at [<phabricator>/src/applications/people/storage/PhabricatorUser.php:432] Daemon 43450 STDE [Mon, 08 Jun 2015 22:49:57 +0000] #2 <#2> PhabricatorUser::generateToken(integer, integer, string, integer) called at [<phabricator>/src/applications/people/storage/PhabricatorUser.php:344] Daemon 43450 STDE [Mon, 08 Jun 2015 22:49:57 +0000] #3 <#2> PhabricatorUser::getRawCSRFToken() called at [<phabricator>/src/applications/people/storage/PhabricatorUser.php:357] Daemon 43450 STDE [Mon, 08 Jun 2015 22:49:57 +0000] #4 <#2> PhabricatorUser::getCSRFToken() called at [<phabricator>/src/infrastructure/javelin/markup.php:91] Daemon 43450 STDE [Mon, 08 Jun 2015 22:49:57 +0000] #5 <#2> phabricator_form(PhabricatorUser, array, array) called at [<phabricator>/src/applications/slowvote/view/SlowvoteEmbedView.php:169] Daemon 43450 STDE [Mon, 08 Jun 2015 22:49:57 +0000] #6 <#2> SlowvoteEmbedView::render() called at [<phabricator>/src/view/AphrontView.php:175] Daemon 43450 STDE [Mon, 08 Jun 2015 22:49:57 +0000] #7 <#2> AphrontView::producePhutilSafeHTML() called at [<phutil>/src/markup/render.php:133] Daemon 43450 STDE [Mon, 08 Jun 2015 22:49:57 +0000] #8 <#2> phutil_escape_html(SlowvoteEmbedView) Daemon 43450 STDE [Mon, 08 Jun 2015 22:49:57 +0000] #9 <#2> array_map(string, array) called at [<phutil>/src/markup/engine/remarkup/PhutilRemarkupBlockStorage.php:56] Daemon 43450 STDE [Mon, 08 Jun 2015 22:49:57 +0000] #10 <#2> PhutilRemarkupBlockStorage::restore(PhutilSafeHTML, integer) called at [<phutil>/src/markup/engine/PhutilRemarkupEngine.php:299] Daemon 43450 STDE [Mon, 08 Jun 2015 22:49:57 +0000] #11 <#2> PhutilRemarkupEngine::restoreText(PhutilSafeHTML, integer) called at [<phutil>/src/markup/engine/PhutilRemarkupEngine.php:295] Daemon 43450 STDE [Mon, 08 Jun 2015 22:49:57 +0000] #12 <#2> PhutilRemarkupEngine::postprocessText(array) called at [<phabricator>/src/infrastructure/markup/PhabricatorMarkupEngine.php:138] Daemon 43450 STDE [Mon, 08 Jun 2015 22:49:57 +0000] #13 <#2> PhabricatorMarkupEngine::process() called at [<phabricator>/src/applications/feed/story/PhabricatorFeedStory.php:167] Daemon 43450 STDE [Mon, 08 Jun 2015 22:49:57 +0000] #14 <#2> PhabricatorFeedStory::loadAllFromRows(array, PhabricatorUser) called at [<phabricator>/src/applications/feed/query/PhabricatorFeedQuery.php:37] Daemon 43450 STDE [Mon, 08 Jun 2015 22:49:57 +0000] #15 <#2> PhabricatorFeedQuery::willFilterPage(array) called at [<phabricator>/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php:237] Daemon 43450 STDE [Mon, 08 Jun 2015 22:49:57 +0000] #16 <#2> PhabricatorPolicyAwareQuery::execute() called at [<phabricator>/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php:168] Daemon 43450 STDE [Mon, 08 Jun 2015 22:49:57 +0000] #17 <#2> PhabricatorPolicyAwareQuery::executeOne() called at [<phabricator>/src/applications/feed/worker/FeedPushWorker.php:12] Daemon 43450 STDE [Mon, 08 Jun 2015 22:49:57 +0000] #18 <#2> FeedPushWorker::loadFeedStory() called at [<phabricator>/src/applications/feed/worker/FeedPublisherWorker.php:6] Daemon 43450 STDE [Mon, 08 Jun 2015 22:49:57 +0000] #19 <#2> FeedPublisherWorker::doWork() called at [<phabricator>/src/infrastructure/daemon/workers/PhabricatorWorker.php:91] Daemon 43450 STDE [Mon, 08 Jun 2015 22:49:57 +0000] #20 <#2> PhabricatorWorker::executeTask() called at [<phabricator>/src/infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php:162] Daemon 43450 STDE [Mon, 08 Jun 2015 22:49:57 +0000] #21 <#2> PhabricatorWorkerActiveTask::executeTask() called at [<phabricator>/src/infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php:22] Daemon 43450 STDE [Mon, 08 Jun 2015 22:49:57 +0000] #22 PhabricatorTaskmasterDaemon::run() called at [<phutil>/src/daemon/PhutilDaemon.php:185] Daemon 43450 STDE [Mon, 08 Jun 2015 22:49:57 +0000] #23 PhutilDaemon::execute() called at [<phutil>/scripts/daemon/exec/exec_daemon.php:125] ``` Just return `null`. Test Plan: Will check that tasks clear in production. Reviewers: btrahan Reviewed By: btrahan Subscribers: epriestley Differential Revision: https://secure.phabricator.com/D13218
2015-06-08 16:07:21 -07:00
if ($this->isOmnipotent()) {
// We may end up here when called from the daemons. The omnipotent user
// has no meaningful CSRF token, so just return `null`.
return null;
}
if ($this->csrfSalt === null) {
$this->csrfSalt = Filesystem::readRandomCharacters(
self::CSRF_SALT_LENGTH);
}
Fix conservative CSRF token cycling limit Summary: We currently cycle CSRF tokens every hour and check for the last two valid ones. This means that a form could go stale in as little as an hour, and is certainly stale after two. When a stale form is submitted, you basically get a terrible heisen-state where some of your data might persist if you're lucky but more likely it all just vanishes. The .js file below outlines some more details. This is a pretty terrible UX and we don't need to be as conservative about CSRF validation as we're being. Remedy this problem by: - Accepting the last 6 CSRF tokens instead of the last 1 (i.e., pages are valid for at least 6 hours, and for as long as 7). - Using JS to refresh the CSRF token every 55 minutes (i.e., pages connected to the internet are valid indefinitely). - Showing the user an explicit message about what went wrong when CSRF validation fails so the experience is less bewildering. They should now only be able to submit with a bad CSRF token if: - They load a page, disconnect from the internet for 7 hours, reconnect, and submit the form within 55 minutes; or - They are actually the victim of a CSRF attack. We could eventually fix the first one by tracking reconnects, which might be "free" once the notification server gets built. It will probably never be an issue in practice. Test Plan: - Reduced CSRF cycle frequency to 2 seconds, submitted a form after 15 seconds, got the CSRF exception. - Reduced csrf-refresh cycle frequency to 3 seconds, submitted a form after 15 seconds, got a clean form post. - Added debugging code the the csrf refresh to make sure it was doing sensible things (pulling different tokens, finding all the inputs). Reviewed By: aran Reviewers: tuomaspelkonen, jungejason, aran CC: aran, epriestley Differential Revision: 660
2011-07-13 14:05:18 -07:00
$salt = $this->csrfSalt;
// Generate a token hash to mitigate BREACH attacks against SSL. See
// discussion in T3684.
$token = $this->getRawCSRFToken();
$hash = PhabricatorHash::digest($token, $salt);
return self::CSRF_BREACH_PREFIX.$salt.substr(
$hash, 0, self::CSRF_TOKEN_LENGTH);
}
public function validateCSRFToken($token) {
// We expect a BREACH-mitigating token. See T3684.
$breach_prefix = self::CSRF_BREACH_PREFIX;
$breach_prelen = strlen($breach_prefix);
if (strncmp($token, $breach_prefix, $breach_prelen) !== 0) {
return false;
}
$salt = substr($token, $breach_prelen, self::CSRF_SALT_LENGTH);
$token = substr($token, $breach_prelen + self::CSRF_SALT_LENGTH);
Fix conservative CSRF token cycling limit Summary: We currently cycle CSRF tokens every hour and check for the last two valid ones. This means that a form could go stale in as little as an hour, and is certainly stale after two. When a stale form is submitted, you basically get a terrible heisen-state where some of your data might persist if you're lucky but more likely it all just vanishes. The .js file below outlines some more details. This is a pretty terrible UX and we don't need to be as conservative about CSRF validation as we're being. Remedy this problem by: - Accepting the last 6 CSRF tokens instead of the last 1 (i.e., pages are valid for at least 6 hours, and for as long as 7). - Using JS to refresh the CSRF token every 55 minutes (i.e., pages connected to the internet are valid indefinitely). - Showing the user an explicit message about what went wrong when CSRF validation fails so the experience is less bewildering. They should now only be able to submit with a bad CSRF token if: - They load a page, disconnect from the internet for 7 hours, reconnect, and submit the form within 55 minutes; or - They are actually the victim of a CSRF attack. We could eventually fix the first one by tracking reconnects, which might be "free" once the notification server gets built. It will probably never be an issue in practice. Test Plan: - Reduced CSRF cycle frequency to 2 seconds, submitted a form after 15 seconds, got the CSRF exception. - Reduced csrf-refresh cycle frequency to 3 seconds, submitted a form after 15 seconds, got a clean form post. - Added debugging code the the csrf refresh to make sure it was doing sensible things (pulling different tokens, finding all the inputs). Reviewed By: aran Reviewers: tuomaspelkonen, jungejason, aran CC: aran, epriestley Differential Revision: 660
2011-07-13 14:05:18 -07:00
// When the user posts a form, we check that it contains a valid CSRF token.
// Tokens cycle each hour (every CSRF_CYLCE_FREQUENCY seconds) and we accept
// either the current token, the next token (users can submit a "future"
// token if you have two web frontends that have some clock skew) or any of
// the last 6 tokens. This means that pages are valid for up to 7 hours.
// There is also some Javascript which periodically refreshes the CSRF
// tokens on each page, so theoretically pages should be valid indefinitely.
// However, this code may fail to run (if the user loses their internet
// connection, or there's a JS problem, or they don't have JS enabled).
// Choosing the size of the window in which we accept old CSRF tokens is
// an issue of balancing concerns between security and usability. We could
// choose a very narrow (e.g., 1-hour) window to reduce vulnerability to
// attacks using captured CSRF tokens, but it's also more likely that real
// users will be affected by this, e.g. if they close their laptop for an
// hour, open it back up, and try to submit a form before the CSRF refresh
// can kick in. Since the user experience of submitting a form with expired
// CSRF is often quite bad (you basically lose data, or it's a big pain to
// recover at least) and I believe we gain little additional protection
// by keeping the window very short (the overwhelming value here is in
// preventing blind attacks, and most attacks which can capture CSRF tokens
// can also just capture authentication information [sniffing networks]
// or act as the user [xss]) the 7 hour default seems like a reasonable
// balance. Other major platforms have much longer CSRF token lifetimes,
// like Rails (session duration) and Django (forever), which suggests this
// is a reasonable analysis.
$csrf_window = 6;
for ($ii = -$csrf_window; $ii <= 1; $ii++) {
$valid = $this->getRawCSRFToken($ii);
$digest = PhabricatorHash::digest($valid, $salt);
$digest = substr($digest, 0, self::CSRF_TOKEN_LENGTH);
if (phutil_hashes_are_identical($digest, $token)) {
return true;
2011-01-30 18:52:29 -08:00
}
}
2011-01-30 18:52:29 -08:00
return false;
}
private function generateToken($epoch, $frequency, $key, $len) {
if ($this->getPHID()) {
$vec = $this->getPHID().$this->getAccountSecret();
} else {
$vec = $this->getAlternateCSRFString();
}
if ($this->hasSession()) {
$vec = $vec.$this->getSession()->getSessionKey();
}
$time_block = floor($epoch / $frequency);
$vec = $vec.$key.$time_block;
return substr(PhabricatorHash::digest($vec), 0, $len);
2011-01-30 18:52:29 -08:00
}
public function getUserProfile() {
return $this->assertAttached($this->profile);
}
public function attachUserProfile(PhabricatorUserProfile $profile) {
$this->profile = $profile;
return $this;
}
public function loadUserProfile() {
if ($this->profile) {
return $this->profile;
}
$profile_dao = new PhabricatorUserProfile();
$this->profile = $profile_dao->loadOneWhere('userPHID = %s',
$this->getPHID());
if (!$this->profile) {
$this->profile = PhabricatorUserProfile::initializeNewProfile($this);
}
return $this->profile;
}
Allow users to have multiple email addresses, and verify emails Summary: - Move email to a separate table. - Migrate existing email to new storage. - Allow users to add and remove email addresses. - Allow users to verify email addresses. - Allow users to change their primary email address. - Convert all the registration/reset/login code to understand these changes. - There are a few security considerations here but I think I've addressed them. Principally, it is important to never let a user acquire a verified email address they don't actually own. We ensure this by tightening the scoping of token generation rules to be (user, email) specific. - This should have essentially zero impact on Facebook, but may require some minor changes in the registration code -- I don't exactly remember how it is set up. Not included here (next steps): - Allow configuration to restrict email to certain domains. - Allow configuration to require validated email. Test Plan: This is a fairly extensive, difficult-to-test change. - From "Email Addresses" interface: - Added new email (verified email verifications sent). - Changed primary email (verified old/new notificactions sent). - Resent verification emails (verified they sent). - Removed email. - Tried to add already-owned email. - Created new users with "accountadmin". Edited existing users with "accountadmin". - Created new users with "add_user.php". - Created new users with web interface. - Clicked welcome email link, verified it verified email. - Reset password. - Linked/unlinked oauth accounts. - Logged in with oauth account. - Logged in with email. - Registered with Oauth account. - Tried to register with OAuth account with duplicate email. - Verified errors for email verification with bad tokens, etc. Reviewers: btrahan, vrana, jungejason Reviewed By: btrahan CC: aran Maniphest Tasks: T1184 Differential Revision: https://secure.phabricator.com/D2393
2012-05-07 10:29:33 -07:00
public function loadPrimaryEmailAddress() {
$email = $this->loadPrimaryEmail();
if (!$email) {
throw new Exception(pht('User has no primary email address!'));
Allow users to have multiple email addresses, and verify emails Summary: - Move email to a separate table. - Migrate existing email to new storage. - Allow users to add and remove email addresses. - Allow users to verify email addresses. - Allow users to change their primary email address. - Convert all the registration/reset/login code to understand these changes. - There are a few security considerations here but I think I've addressed them. Principally, it is important to never let a user acquire a verified email address they don't actually own. We ensure this by tightening the scoping of token generation rules to be (user, email) specific. - This should have essentially zero impact on Facebook, but may require some minor changes in the registration code -- I don't exactly remember how it is set up. Not included here (next steps): - Allow configuration to restrict email to certain domains. - Allow configuration to require validated email. Test Plan: This is a fairly extensive, difficult-to-test change. - From "Email Addresses" interface: - Added new email (verified email verifications sent). - Changed primary email (verified old/new notificactions sent). - Resent verification emails (verified they sent). - Removed email. - Tried to add already-owned email. - Created new users with "accountadmin". Edited existing users with "accountadmin". - Created new users with "add_user.php". - Created new users with web interface. - Clicked welcome email link, verified it verified email. - Reset password. - Linked/unlinked oauth accounts. - Logged in with oauth account. - Logged in with email. - Registered with Oauth account. - Tried to register with OAuth account with duplicate email. - Verified errors for email verification with bad tokens, etc. Reviewers: btrahan, vrana, jungejason Reviewed By: btrahan CC: aran Maniphest Tasks: T1184 Differential Revision: https://secure.phabricator.com/D2393
2012-05-07 10:29:33 -07:00
}
return $email->getAddress();
}
public function loadPrimaryEmail() {
return $this->loadOneRelative(
new PhabricatorUserEmail(),
'userPHID',
'getPHID',
'(isPrimary = 1)');
}
/* -( Settings )----------------------------------------------------------- */
Provide a general-purpose, modular user cache for settings and other similar data Summary: Ref T4103. Currently, we issue a `SELECT * FROM user_preferences ... WHERE userPHID = ...` on every page to load the viewer's settings. There are several other questionable data accesses on every page too, most of which could benefit from improved caching strategies (see T4103#178122). This query will soon get more expensive, since it may need to load several objects (e.g., the user's settings and their "role profile" settings). Although we could put that data on the User and do both in one query, it's nicer to put it on the Preferences object ("This inherits from profile X") which means we need to do several queries. Rather than paying a greater price, we can cheat this stuff into the existing query where we load the user's session by providing a user cache table and doing some JOIN magic. This lets us issue one query and try to get cache hits on a bunch of caches cheaply (well, we'll be in trouble at the MySQL JOIN limit of 61 tables, but have some headroom). For now, just get it working: - Add the table. - Try to get user settings "for free" when we load the session. - If we miss, fill user settings into the cache on-demand. - We only use this in one place (DarkConsole) for now. I'll use it more widely in the next diff. Test Plan: - Loaded page as logged-in user. - Loaded page as logged-out user. - Examined session query to see cache joins. - Changed settings, saw database cache fill. - Toggled DarkConsole on and off. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4103 Differential Revision: https://secure.phabricator.com/D16001
2016-06-01 12:14:39 -07:00
public function getUserSetting($key) {
Continue modernizing application access to user preferences Summary: Ref T4103. This is just incremental cleanup: - Add "internal" settings, which aren't editable via the UI. They can still do validation and run through the normal pathway. Move a couple settings to use this. - Remove `getPreference()` on `PhabricatorUser`, which was a sort of prototype version of `getUserSetting()`. - Make `getUserSetting()` validate setting values before returning them, to improve robustness if we change allowable values later. - Add a user setting cache, since reading user settings was getting fairly expensive on Calendar. - Improve performance of setting validation for timezone setting (don't require building/computing all timezone offsets). - Since we have the cache anyway, make the timezone override a little more general in its approach. - Move editor stuff to use `getUserSetting()`. Test Plan: - Changed search scopes. - Reconciled local and server timezone settings by ignoring and changing timezones. - Changed date/time settings, browsed Calendar, queried date ranges. - Verified editor links generate properly in Diffusion. - Browsed around with time/date settings looking at timestamps. - Grepped for `getPreference()`, nuked all the ones coming off `$user` or `$viewer` that I could find. - Changed accessiblity to high-contrast colors. - Ran all unit tests. - Grepped for removed constants. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4103 Differential Revision: https://secure.phabricator.com/D16015
2016-06-02 17:12:15 -07:00
// NOTE: We store available keys and cached values separately to make it
// faster to check for `null` in the cache, which is common.
if (isset($this->settingCacheKeys[$key])) {
return $this->settingCache[$key];
}
Provide a general-purpose, modular user cache for settings and other similar data Summary: Ref T4103. Currently, we issue a `SELECT * FROM user_preferences ... WHERE userPHID = ...` on every page to load the viewer's settings. There are several other questionable data accesses on every page too, most of which could benefit from improved caching strategies (see T4103#178122). This query will soon get more expensive, since it may need to load several objects (e.g., the user's settings and their "role profile" settings). Although we could put that data on the User and do both in one query, it's nicer to put it on the Preferences object ("This inherits from profile X") which means we need to do several queries. Rather than paying a greater price, we can cheat this stuff into the existing query where we load the user's session by providing a user cache table and doing some JOIN magic. This lets us issue one query and try to get cache hits on a bunch of caches cheaply (well, we'll be in trouble at the MySQL JOIN limit of 61 tables, but have some headroom). For now, just get it working: - Add the table. - Try to get user settings "for free" when we load the session. - If we miss, fill user settings into the cache on-demand. - We only use this in one place (DarkConsole) for now. I'll use it more widely in the next diff. Test Plan: - Loaded page as logged-in user. - Loaded page as logged-out user. - Examined session query to see cache joins. - Changed settings, saw database cache fill. - Toggled DarkConsole on and off. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4103 Differential Revision: https://secure.phabricator.com/D16001
2016-06-01 12:14:39 -07:00
$settings_key = PhabricatorUserPreferencesCacheType::KEY_PREFERENCES;
if ($this->getPHID()) {
$settings = $this->requireCacheData($settings_key);
} else {
$settings = $this->loadGlobalSettings();
}
Provide a general-purpose, modular user cache for settings and other similar data Summary: Ref T4103. Currently, we issue a `SELECT * FROM user_preferences ... WHERE userPHID = ...` on every page to load the viewer's settings. There are several other questionable data accesses on every page too, most of which could benefit from improved caching strategies (see T4103#178122). This query will soon get more expensive, since it may need to load several objects (e.g., the user's settings and their "role profile" settings). Although we could put that data on the User and do both in one query, it's nicer to put it on the Preferences object ("This inherits from profile X") which means we need to do several queries. Rather than paying a greater price, we can cheat this stuff into the existing query where we load the user's session by providing a user cache table and doing some JOIN magic. This lets us issue one query and try to get cache hits on a bunch of caches cheaply (well, we'll be in trouble at the MySQL JOIN limit of 61 tables, but have some headroom). For now, just get it working: - Add the table. - Try to get user settings "for free" when we load the session. - If we miss, fill user settings into the cache on-demand. - We only use this in one place (DarkConsole) for now. I'll use it more widely in the next diff. Test Plan: - Loaded page as logged-in user. - Loaded page as logged-out user. - Examined session query to see cache joins. - Changed settings, saw database cache fill. - Toggled DarkConsole on and off. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4103 Differential Revision: https://secure.phabricator.com/D16001
2016-06-01 12:14:39 -07:00
if (array_key_exists($key, $settings)) {
Continue modernizing application access to user preferences Summary: Ref T4103. This is just incremental cleanup: - Add "internal" settings, which aren't editable via the UI. They can still do validation and run through the normal pathway. Move a couple settings to use this. - Remove `getPreference()` on `PhabricatorUser`, which was a sort of prototype version of `getUserSetting()`. - Make `getUserSetting()` validate setting values before returning them, to improve robustness if we change allowable values later. - Add a user setting cache, since reading user settings was getting fairly expensive on Calendar. - Improve performance of setting validation for timezone setting (don't require building/computing all timezone offsets). - Since we have the cache anyway, make the timezone override a little more general in its approach. - Move editor stuff to use `getUserSetting()`. Test Plan: - Changed search scopes. - Reconciled local and server timezone settings by ignoring and changing timezones. - Changed date/time settings, browsed Calendar, queried date ranges. - Verified editor links generate properly in Diffusion. - Browsed around with time/date settings looking at timestamps. - Grepped for `getPreference()`, nuked all the ones coming off `$user` or `$viewer` that I could find. - Changed accessiblity to high-contrast colors. - Ran all unit tests. - Grepped for removed constants. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4103 Differential Revision: https://secure.phabricator.com/D16015
2016-06-02 17:12:15 -07:00
$value = $settings[$key];
return $this->writeUserSettingCache($key, $value);
}
$cache = PhabricatorCaches::getRuntimeCache();
$cache_key = "settings.defaults({$key})";
$cache_map = $cache->getKeys(array($cache_key));
Continue modernizing application access to user preferences Summary: Ref T4103. This is just incremental cleanup: - Add "internal" settings, which aren't editable via the UI. They can still do validation and run through the normal pathway. Move a couple settings to use this. - Remove `getPreference()` on `PhabricatorUser`, which was a sort of prototype version of `getUserSetting()`. - Make `getUserSetting()` validate setting values before returning them, to improve robustness if we change allowable values later. - Add a user setting cache, since reading user settings was getting fairly expensive on Calendar. - Improve performance of setting validation for timezone setting (don't require building/computing all timezone offsets). - Since we have the cache anyway, make the timezone override a little more general in its approach. - Move editor stuff to use `getUserSetting()`. Test Plan: - Changed search scopes. - Reconciled local and server timezone settings by ignoring and changing timezones. - Changed date/time settings, browsed Calendar, queried date ranges. - Verified editor links generate properly in Diffusion. - Browsed around with time/date settings looking at timestamps. - Grepped for `getPreference()`, nuked all the ones coming off `$user` or `$viewer` that I could find. - Changed accessiblity to high-contrast colors. - Ran all unit tests. - Grepped for removed constants. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4103 Differential Revision: https://secure.phabricator.com/D16015
2016-06-02 17:12:15 -07:00
if ($cache_map) {
$value = $cache_map[$cache_key];
} else {
$defaults = PhabricatorSetting::getAllSettings();
Continue modernizing application access to user preferences Summary: Ref T4103. This is just incremental cleanup: - Add "internal" settings, which aren't editable via the UI. They can still do validation and run through the normal pathway. Move a couple settings to use this. - Remove `getPreference()` on `PhabricatorUser`, which was a sort of prototype version of `getUserSetting()`. - Make `getUserSetting()` validate setting values before returning them, to improve robustness if we change allowable values later. - Add a user setting cache, since reading user settings was getting fairly expensive on Calendar. - Improve performance of setting validation for timezone setting (don't require building/computing all timezone offsets). - Since we have the cache anyway, make the timezone override a little more general in its approach. - Move editor stuff to use `getUserSetting()`. Test Plan: - Changed search scopes. - Reconciled local and server timezone settings by ignoring and changing timezones. - Changed date/time settings, browsed Calendar, queried date ranges. - Verified editor links generate properly in Diffusion. - Browsed around with time/date settings looking at timestamps. - Grepped for `getPreference()`, nuked all the ones coming off `$user` or `$viewer` that I could find. - Changed accessiblity to high-contrast colors. - Ran all unit tests. - Grepped for removed constants. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4103 Differential Revision: https://secure.phabricator.com/D16015
2016-06-02 17:12:15 -07:00
if (isset($defaults[$key])) {
$value = id(clone $defaults[$key])
->setViewer($this)
->getSettingDefaultValue();
} else {
$value = null;
Continue modernizing application access to user preferences Summary: Ref T4103. This is just incremental cleanup: - Add "internal" settings, which aren't editable via the UI. They can still do validation and run through the normal pathway. Move a couple settings to use this. - Remove `getPreference()` on `PhabricatorUser`, which was a sort of prototype version of `getUserSetting()`. - Make `getUserSetting()` validate setting values before returning them, to improve robustness if we change allowable values later. - Add a user setting cache, since reading user settings was getting fairly expensive on Calendar. - Improve performance of setting validation for timezone setting (don't require building/computing all timezone offsets). - Since we have the cache anyway, make the timezone override a little more general in its approach. - Move editor stuff to use `getUserSetting()`. Test Plan: - Changed search scopes. - Reconciled local and server timezone settings by ignoring and changing timezones. - Changed date/time settings, browsed Calendar, queried date ranges. - Verified editor links generate properly in Diffusion. - Browsed around with time/date settings looking at timestamps. - Grepped for `getPreference()`, nuked all the ones coming off `$user` or `$viewer` that I could find. - Changed accessiblity to high-contrast colors. - Ran all unit tests. - Grepped for removed constants. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4103 Differential Revision: https://secure.phabricator.com/D16015
2016-06-02 17:12:15 -07:00
}
Provide a general-purpose, modular user cache for settings and other similar data Summary: Ref T4103. Currently, we issue a `SELECT * FROM user_preferences ... WHERE userPHID = ...` on every page to load the viewer's settings. There are several other questionable data accesses on every page too, most of which could benefit from improved caching strategies (see T4103#178122). This query will soon get more expensive, since it may need to load several objects (e.g., the user's settings and their "role profile" settings). Although we could put that data on the User and do both in one query, it's nicer to put it on the Preferences object ("This inherits from profile X") which means we need to do several queries. Rather than paying a greater price, we can cheat this stuff into the existing query where we load the user's session by providing a user cache table and doing some JOIN magic. This lets us issue one query and try to get cache hits on a bunch of caches cheaply (well, we'll be in trouble at the MySQL JOIN limit of 61 tables, but have some headroom). For now, just get it working: - Add the table. - Try to get user settings "for free" when we load the session. - If we miss, fill user settings into the cache on-demand. - We only use this in one place (DarkConsole) for now. I'll use it more widely in the next diff. Test Plan: - Loaded page as logged-in user. - Loaded page as logged-out user. - Examined session query to see cache joins. - Changed settings, saw database cache fill. - Toggled DarkConsole on and off. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4103 Differential Revision: https://secure.phabricator.com/D16001
2016-06-01 12:14:39 -07:00
$cache->setKey($cache_key, $value);
Provide a general-purpose, modular user cache for settings and other similar data Summary: Ref T4103. Currently, we issue a `SELECT * FROM user_preferences ... WHERE userPHID = ...` on every page to load the viewer's settings. There are several other questionable data accesses on every page too, most of which could benefit from improved caching strategies (see T4103#178122). This query will soon get more expensive, since it may need to load several objects (e.g., the user's settings and their "role profile" settings). Although we could put that data on the User and do both in one query, it's nicer to put it on the Preferences object ("This inherits from profile X") which means we need to do several queries. Rather than paying a greater price, we can cheat this stuff into the existing query where we load the user's session by providing a user cache table and doing some JOIN magic. This lets us issue one query and try to get cache hits on a bunch of caches cheaply (well, we'll be in trouble at the MySQL JOIN limit of 61 tables, but have some headroom). For now, just get it working: - Add the table. - Try to get user settings "for free" when we load the session. - If we miss, fill user settings into the cache on-demand. - We only use this in one place (DarkConsole) for now. I'll use it more widely in the next diff. Test Plan: - Loaded page as logged-in user. - Loaded page as logged-out user. - Examined session query to see cache joins. - Changed settings, saw database cache fill. - Toggled DarkConsole on and off. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4103 Differential Revision: https://secure.phabricator.com/D16001
2016-06-01 12:14:39 -07:00
}
return $this->writeUserSettingCache($key, $value);
Provide a general-purpose, modular user cache for settings and other similar data Summary: Ref T4103. Currently, we issue a `SELECT * FROM user_preferences ... WHERE userPHID = ...` on every page to load the viewer's settings. There are several other questionable data accesses on every page too, most of which could benefit from improved caching strategies (see T4103#178122). This query will soon get more expensive, since it may need to load several objects (e.g., the user's settings and their "role profile" settings). Although we could put that data on the User and do both in one query, it's nicer to put it on the Preferences object ("This inherits from profile X") which means we need to do several queries. Rather than paying a greater price, we can cheat this stuff into the existing query where we load the user's session by providing a user cache table and doing some JOIN magic. This lets us issue one query and try to get cache hits on a bunch of caches cheaply (well, we'll be in trouble at the MySQL JOIN limit of 61 tables, but have some headroom). For now, just get it working: - Add the table. - Try to get user settings "for free" when we load the session. - If we miss, fill user settings into the cache on-demand. - We only use this in one place (DarkConsole) for now. I'll use it more widely in the next diff. Test Plan: - Loaded page as logged-in user. - Loaded page as logged-out user. - Examined session query to see cache joins. - Changed settings, saw database cache fill. - Toggled DarkConsole on and off. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4103 Differential Revision: https://secure.phabricator.com/D16001
2016-06-01 12:14:39 -07:00
}
/**
* Test if a given setting is set to a particular value.
*
* @param const Setting key.
* @param wild Value to compare.
* @return bool True if the setting has the specified value.
* @task settings
*/
public function compareUserSetting($key, $value) {
$actual = $this->getUserSetting($key);
return ($actual == $value);
}
private function writeUserSettingCache($key, $value) {
$this->settingCacheKeys[$key] = true;
$this->settingCache[$key] = $value;
return $value;
}
public function getTranslation() {
return $this->getUserSetting(PhabricatorTranslationSetting::SETTINGKEY);
}
public function getTimezoneIdentifier() {
return $this->getUserSetting(PhabricatorTimezoneSetting::SETTINGKEY);
}
public static function getGlobalSettingsCacheKey() {
return 'user.settings.globals.v1';
}
private function loadGlobalSettings() {
$cache_key = self::getGlobalSettingsCacheKey();
$cache = PhabricatorCaches::getMutableStructureCache();
$settings = $cache->getKey($cache_key);
if (!$settings) {
$preferences = PhabricatorUserPreferences::loadGlobalPreferences($this);
$settings = $preferences->getPreferences();
$cache->setKey($cache_key, $settings);
}
return $settings;
}
/**
* Override the user's timezone identifier.
*
* This is primarily useful for unit tests.
*
* @param string New timezone identifier.
* @return this
* @task settings
*/
public function overrideTimezoneIdentifier($identifier) {
Continue modernizing application access to user preferences Summary: Ref T4103. This is just incremental cleanup: - Add "internal" settings, which aren't editable via the UI. They can still do validation and run through the normal pathway. Move a couple settings to use this. - Remove `getPreference()` on `PhabricatorUser`, which was a sort of prototype version of `getUserSetting()`. - Make `getUserSetting()` validate setting values before returning them, to improve robustness if we change allowable values later. - Add a user setting cache, since reading user settings was getting fairly expensive on Calendar. - Improve performance of setting validation for timezone setting (don't require building/computing all timezone offsets). - Since we have the cache anyway, make the timezone override a little more general in its approach. - Move editor stuff to use `getUserSetting()`. Test Plan: - Changed search scopes. - Reconciled local and server timezone settings by ignoring and changing timezones. - Changed date/time settings, browsed Calendar, queried date ranges. - Verified editor links generate properly in Diffusion. - Browsed around with time/date settings looking at timestamps. - Grepped for `getPreference()`, nuked all the ones coming off `$user` or `$viewer` that I could find. - Changed accessiblity to high-contrast colors. - Ran all unit tests. - Grepped for removed constants. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4103 Differential Revision: https://secure.phabricator.com/D16015
2016-06-02 17:12:15 -07:00
$timezone_key = PhabricatorTimezoneSetting::SETTINGKEY;
$this->settingCacheKeys[$timezone_key] = true;
$this->settingCache[$timezone_key] = $identifier;
return $this;
}
public function getGender() {
return $this->getUserSetting(PhabricatorPronounSetting::SETTINGKEY);
}
public function loadEditorLink(
$path,
$line,
PhabricatorRepository $repository = null) {
Continue modernizing application access to user preferences Summary: Ref T4103. This is just incremental cleanup: - Add "internal" settings, which aren't editable via the UI. They can still do validation and run through the normal pathway. Move a couple settings to use this. - Remove `getPreference()` on `PhabricatorUser`, which was a sort of prototype version of `getUserSetting()`. - Make `getUserSetting()` validate setting values before returning them, to improve robustness if we change allowable values later. - Add a user setting cache, since reading user settings was getting fairly expensive on Calendar. - Improve performance of setting validation for timezone setting (don't require building/computing all timezone offsets). - Since we have the cache anyway, make the timezone override a little more general in its approach. - Move editor stuff to use `getUserSetting()`. Test Plan: - Changed search scopes. - Reconciled local and server timezone settings by ignoring and changing timezones. - Changed date/time settings, browsed Calendar, queried date ranges. - Verified editor links generate properly in Diffusion. - Browsed around with time/date settings looking at timestamps. - Grepped for `getPreference()`, nuked all the ones coming off `$user` or `$viewer` that I could find. - Changed accessiblity to high-contrast colors. - Ran all unit tests. - Grepped for removed constants. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4103 Differential Revision: https://secure.phabricator.com/D16015
2016-06-02 17:12:15 -07:00
$editor = $this->getUserSetting(PhabricatorEditorSetting::SETTINGKEY);
if (is_array($path)) {
Continue modernizing application access to user preferences Summary: Ref T4103. This is just incremental cleanup: - Add "internal" settings, which aren't editable via the UI. They can still do validation and run through the normal pathway. Move a couple settings to use this. - Remove `getPreference()` on `PhabricatorUser`, which was a sort of prototype version of `getUserSetting()`. - Make `getUserSetting()` validate setting values before returning them, to improve robustness if we change allowable values later. - Add a user setting cache, since reading user settings was getting fairly expensive on Calendar. - Improve performance of setting validation for timezone setting (don't require building/computing all timezone offsets). - Since we have the cache anyway, make the timezone override a little more general in its approach. - Move editor stuff to use `getUserSetting()`. Test Plan: - Changed search scopes. - Reconciled local and server timezone settings by ignoring and changing timezones. - Changed date/time settings, browsed Calendar, queried date ranges. - Verified editor links generate properly in Diffusion. - Browsed around with time/date settings looking at timestamps. - Grepped for `getPreference()`, nuked all the ones coming off `$user` or `$viewer` that I could find. - Changed accessiblity to high-contrast colors. - Ran all unit tests. - Grepped for removed constants. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4103 Differential Revision: https://secure.phabricator.com/D16015
2016-06-02 17:12:15 -07:00
$multi_key = PhabricatorEditorMultipleSetting::SETTINGKEY;
$multiedit = $this->getUserSetting($multi_key);
switch ($multiedit) {
Continue modernizing application access to user preferences Summary: Ref T4103. This is just incremental cleanup: - Add "internal" settings, which aren't editable via the UI. They can still do validation and run through the normal pathway. Move a couple settings to use this. - Remove `getPreference()` on `PhabricatorUser`, which was a sort of prototype version of `getUserSetting()`. - Make `getUserSetting()` validate setting values before returning them, to improve robustness if we change allowable values later. - Add a user setting cache, since reading user settings was getting fairly expensive on Calendar. - Improve performance of setting validation for timezone setting (don't require building/computing all timezone offsets). - Since we have the cache anyway, make the timezone override a little more general in its approach. - Move editor stuff to use `getUserSetting()`. Test Plan: - Changed search scopes. - Reconciled local and server timezone settings by ignoring and changing timezones. - Changed date/time settings, browsed Calendar, queried date ranges. - Verified editor links generate properly in Diffusion. - Browsed around with time/date settings looking at timestamps. - Grepped for `getPreference()`, nuked all the ones coming off `$user` or `$viewer` that I could find. - Changed accessiblity to high-contrast colors. - Ran all unit tests. - Grepped for removed constants. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4103 Differential Revision: https://secure.phabricator.com/D16015
2016-06-02 17:12:15 -07:00
case PhabricatorEditorMultipleSetting::VALUE_SPACES:
$path = implode(' ', $path);
break;
Continue modernizing application access to user preferences Summary: Ref T4103. This is just incremental cleanup: - Add "internal" settings, which aren't editable via the UI. They can still do validation and run through the normal pathway. Move a couple settings to use this. - Remove `getPreference()` on `PhabricatorUser`, which was a sort of prototype version of `getUserSetting()`. - Make `getUserSetting()` validate setting values before returning them, to improve robustness if we change allowable values later. - Add a user setting cache, since reading user settings was getting fairly expensive on Calendar. - Improve performance of setting validation for timezone setting (don't require building/computing all timezone offsets). - Since we have the cache anyway, make the timezone override a little more general in its approach. - Move editor stuff to use `getUserSetting()`. Test Plan: - Changed search scopes. - Reconciled local and server timezone settings by ignoring and changing timezones. - Changed date/time settings, browsed Calendar, queried date ranges. - Verified editor links generate properly in Diffusion. - Browsed around with time/date settings looking at timestamps. - Grepped for `getPreference()`, nuked all the ones coming off `$user` or `$viewer` that I could find. - Changed accessiblity to high-contrast colors. - Ran all unit tests. - Grepped for removed constants. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4103 Differential Revision: https://secure.phabricator.com/D16015
2016-06-02 17:12:15 -07:00
case PhabricatorEditorMultipleSetting::VALUE_SINGLE:
default:
return null;
}
}
if (!strlen($editor)) {
return null;
}
if ($repository) {
$callsign = $repository->getCallsign();
} else {
$callsign = null;
}
$uri = strtr($editor, array(
'%%' => '%',
'%f' => phutil_escape_uri($path),
'%l' => phutil_escape_uri($line),
'%r' => phutil_escape_uri($callsign),
));
// The resulting URI must have an allowed protocol. Otherwise, we'll return
// a link to an error page explaining the misconfiguration.
$ok = PhabricatorHelpEditorProtocolController::hasAllowedProtocol($uri);
if (!$ok) {
return '/help/editorprotocol/';
}
return (string)$uri;
}
public function getAlternateCSRFString() {
return $this->assertAttached($this->alternateCSRFString);
}
public function attachAlternateCSRFString($string) {
$this->alternateCSRFString = $string;
return $this;
}
/**
* Populate the nametoken table, which used to fetch typeahead results. When
* a user types "linc", we want to match "Abraham Lincoln" from on-demand
* typeahead sources. To do this, we need a separate table of name fragments.
*/
public function updateNameTokens() {
$table = self::NAMETOKEN_TABLE;
$conn_w = $this->establishConnection('w');
$tokens = PhabricatorTypeaheadDatasource::tokenizeString(
$this->getUserName().' '.$this->getRealName());
$sql = array();
foreach ($tokens as $token) {
$sql[] = qsprintf(
$conn_w,
'(%d, %s)',
$this->getID(),
$token);
}
queryfx(
$conn_w,
'DELETE FROM %T WHERE userID = %d',
$table,
$this->getID());
if ($sql) {
queryfx(
$conn_w,
'INSERT INTO %T (userID, token) VALUES %Q',
$table,
implode(', ', $sql));
}
}
public function sendWelcomeEmail(PhabricatorUser $admin) {
if (!$this->canEstablishWebSessions()) {
throw new Exception(
pht(
'Can not send welcome mail to users who can not establish '.
'web sessions!'));
}
$admin_username = $admin->getUserName();
$admin_realname = $admin->getRealName();
$user_username = $this->getUserName();
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
$base_uri = PhabricatorEnv::getProductionURI('/');
Make password reset emails use one-time tokens Summary: Ref T4398. This code hadn't been touched in a while and had a few crufty bits. **One Time Resets**: Currently, password reset (and similar links) are valid for about 48 hours, but we always use one token to generate them (it's bound to the account). This isn't horrible, but it could be better, and it produces a lot of false positives on HackerOne. Instead, use TemporaryTokens to make each link one-time only and good for no more than 24 hours. **Coupling of Email Verification and One-Time Login**: Currently, one-time login links ("password reset links") are tightly bound to an email address, and using a link verifies that email address. This is convenient for "Welcome" emails, so the user doesn't need to go through two rounds of checking email in order to login, then very their email, then actually get access to Phabricator. However, for other types of these links (like those generated by `bin/auth recover`) there's no need to do any email verification. Instead, make the email verification part optional, and use it on welcome links but not other types of links. **Message Customization**: These links can come out of several workflows: welcome, password reset, username change, or `bin/auth recover`. Add a hint to the URI so the text on the page can be customized a bit to help users through the workflow. **Reset Emails Going to Main Account Email**: Previously, we would send password reset email to the user's primary account email. However, since we verify email coming from reset links this isn't correct and could allow a user to verify an email without actually controlling it. Since the user needs a real account in the first place this does not seem useful on its own, but might be a component in some other attack. The user might also no longer have access to their primary account, in which case this wouldn't be wrong, but would not be very useful. Mitigate this in two ways: - First, send to the actual email address the user entered, not the primary account email address. - Second, don't let these links verify emails: they're just login links. This primarily makes it more difficult for an attacker to add someone else's email to their account, send them a reset link, get them to login and implicitly verify the email by not reading very carefully, and then figure out something interesting to do (there's currently no followup attack here, but allowing this does seem undesirable). **Password Reset Without Old Password**: After a user logs in via email, we send them to the password settings panel (if passwords are enabled) with a code that lets them set a new password without knowing the old one. Previously, this code was static and based on the email address. Instead, issue a one-time code. **Jump Into Hisec**: Normally, when a user who has multi-factor auth on their account logs in, we prompt them for factors but don't put them in high security. You usually don't want to go do high-security stuff immediately after login, and it would be confusing and annoying if normal logins gave you a "YOU ARE IN HIGH SECURITY" alert bubble. However, if we're taking you to the password reset screen, we //do// want to put the user in high security, since that screen requires high security. If we don't do this, the user gets two factor prompts in a row. To accomplish this, we set a cookie when we know we're sending the user into a high security workflow. This cookie makes login finalization upgrade all the way from "partial" to "high security", instead of stopping halfway at "normal". This is safe because the user has just passed a factor check; the only reason we don't normally do this is to reduce annoyance. **Some UI Cleanup**: Some of this was using really old UI. Modernize it a bit. Test Plan: - **One Time Resets** - Used a reset link. - Tried to reuse a reset link, got denied. - Verified each link is different. - **Coupling of Email Verification and One-Time Login** - Verified that `bin/auth`, password reset, and username change links do not have an email verifying URI component. - Tried to tack one on, got denied. - Used the welcome email link to login + verify. - Tried to mutate the URI to not verify, or verify something else: got denied. - **Message Customization** - Viewed messages on the different workflows. They seemed OK. - **Reset Emails Going to Main Account Email** - Sent password reset email to non-primary email. - Received email at specified address. - Verified it does not verify the address. - **Password Reset Without Old Password** - Reset password without knowledge of old one after email reset. - Tried to do that without a key, got denied. - Tried to reuse a key, got denied. - **Jump Into Hisec** - Logged in with MFA user, got factor'd, jumped directly into hisec. - Logged in with non-MFA user, no factors, normal password reset. - **Some UI Cleanup** - Viewed new UI. - **Misc** - Created accounts, logged in with welcome link, got verified. - Changed a username, used link to log back in. Reviewers: btrahan Reviewed By: btrahan Subscribers: epriestley Maniphest Tasks: T4398 Differential Revision: https://secure.phabricator.com/D9252
2014-05-22 10:41:00 -07:00
$engine = new PhabricatorAuthSessionEngine();
$uri = $engine->getOneTimeLoginURI(
$this,
$this->loadPrimaryEmail(),
PhabricatorAuthSessionEngine::ONETIME_WELCOME);
$body = pht(
"Welcome to Phabricator!\n\n".
"%s (%s) has created an account for you.\n\n".
" Username: %s\n\n".
"To login to Phabricator, follow this link and set a password:\n\n".
" %s\n\n".
"After you have set a password, you can login in the future by ".
"going here:\n\n".
" %s\n",
$admin_username,
$admin_realname,
$user_username,
$uri,
$base_uri);
if (!$is_serious) {
$body .= sprintf(
"\n%s\n",
pht("Love,\nPhabricator"));
}
$mail = id(new PhabricatorMetaMTAMail())
->addTos(array($this->getPHID()))
->setForceDelivery(true)
->setSubject(pht('[Phabricator] Welcome to Phabricator'))
->setBody($body)
->saveAndSend();
}
public function sendUsernameChangeEmail(
PhabricatorUser $admin,
$old_username) {
$admin_username = $admin->getUserName();
$admin_realname = $admin->getRealName();
$new_username = $this->getUserName();
$password_instructions = null;
if (PhabricatorPasswordAuthProvider::getPasswordProvider()) {
Make password reset emails use one-time tokens Summary: Ref T4398. This code hadn't been touched in a while and had a few crufty bits. **One Time Resets**: Currently, password reset (and similar links) are valid for about 48 hours, but we always use one token to generate them (it's bound to the account). This isn't horrible, but it could be better, and it produces a lot of false positives on HackerOne. Instead, use TemporaryTokens to make each link one-time only and good for no more than 24 hours. **Coupling of Email Verification and One-Time Login**: Currently, one-time login links ("password reset links") are tightly bound to an email address, and using a link verifies that email address. This is convenient for "Welcome" emails, so the user doesn't need to go through two rounds of checking email in order to login, then very their email, then actually get access to Phabricator. However, for other types of these links (like those generated by `bin/auth recover`) there's no need to do any email verification. Instead, make the email verification part optional, and use it on welcome links but not other types of links. **Message Customization**: These links can come out of several workflows: welcome, password reset, username change, or `bin/auth recover`. Add a hint to the URI so the text on the page can be customized a bit to help users through the workflow. **Reset Emails Going to Main Account Email**: Previously, we would send password reset email to the user's primary account email. However, since we verify email coming from reset links this isn't correct and could allow a user to verify an email without actually controlling it. Since the user needs a real account in the first place this does not seem useful on its own, but might be a component in some other attack. The user might also no longer have access to their primary account, in which case this wouldn't be wrong, but would not be very useful. Mitigate this in two ways: - First, send to the actual email address the user entered, not the primary account email address. - Second, don't let these links verify emails: they're just login links. This primarily makes it more difficult for an attacker to add someone else's email to their account, send them a reset link, get them to login and implicitly verify the email by not reading very carefully, and then figure out something interesting to do (there's currently no followup attack here, but allowing this does seem undesirable). **Password Reset Without Old Password**: After a user logs in via email, we send them to the password settings panel (if passwords are enabled) with a code that lets them set a new password without knowing the old one. Previously, this code was static and based on the email address. Instead, issue a one-time code. **Jump Into Hisec**: Normally, when a user who has multi-factor auth on their account logs in, we prompt them for factors but don't put them in high security. You usually don't want to go do high-security stuff immediately after login, and it would be confusing and annoying if normal logins gave you a "YOU ARE IN HIGH SECURITY" alert bubble. However, if we're taking you to the password reset screen, we //do// want to put the user in high security, since that screen requires high security. If we don't do this, the user gets two factor prompts in a row. To accomplish this, we set a cookie when we know we're sending the user into a high security workflow. This cookie makes login finalization upgrade all the way from "partial" to "high security", instead of stopping halfway at "normal". This is safe because the user has just passed a factor check; the only reason we don't normally do this is to reduce annoyance. **Some UI Cleanup**: Some of this was using really old UI. Modernize it a bit. Test Plan: - **One Time Resets** - Used a reset link. - Tried to reuse a reset link, got denied. - Verified each link is different. - **Coupling of Email Verification and One-Time Login** - Verified that `bin/auth`, password reset, and username change links do not have an email verifying URI component. - Tried to tack one on, got denied. - Used the welcome email link to login + verify. - Tried to mutate the URI to not verify, or verify something else: got denied. - **Message Customization** - Viewed messages on the different workflows. They seemed OK. - **Reset Emails Going to Main Account Email** - Sent password reset email to non-primary email. - Received email at specified address. - Verified it does not verify the address. - **Password Reset Without Old Password** - Reset password without knowledge of old one after email reset. - Tried to do that without a key, got denied. - Tried to reuse a key, got denied. - **Jump Into Hisec** - Logged in with MFA user, got factor'd, jumped directly into hisec. - Logged in with non-MFA user, no factors, normal password reset. - **Some UI Cleanup** - Viewed new UI. - **Misc** - Created accounts, logged in with welcome link, got verified. - Changed a username, used link to log back in. Reviewers: btrahan Reviewed By: btrahan Subscribers: epriestley Maniphest Tasks: T4398 Differential Revision: https://secure.phabricator.com/D9252
2014-05-22 10:41:00 -07:00
$engine = new PhabricatorAuthSessionEngine();
$uri = $engine->getOneTimeLoginURI(
$this,
null,
PhabricatorAuthSessionEngine::ONETIME_USERNAME);
$password_instructions = sprintf(
"%s\n\n %s\n\n%s\n",
pht(
"If you use a password to login, you'll need to reset it ".
"before you can login again. You can reset your password by ".
"following this link:"),
$uri,
pht(
"And, of course, you'll need to use your new username to login ".
"from now on. If you use OAuth to login, nothing should change."));
}
$body = sprintf(
"%s\n\n %s\n %s\n\n%s",
pht(
'%s (%s) has changed your Phabricator username.',
$admin_username,
$admin_realname),
pht(
'Old Username: %s',
$old_username),
pht(
'New Username: %s',
$new_username),
$password_instructions);
$mail = id(new PhabricatorMetaMTAMail())
->addTos(array($this->getPHID()))
->setForceDelivery(true)
->setSubject(pht('[Phabricator] Username Changed'))
->setBody($body)
->saveAndSend();
}
public static function describeValidUsername() {
return pht(
'Usernames must contain only numbers, letters, period, underscore and '.
'hyphen, and can not end with a period. They must have no more than %d '.
'characters.',
new PhutilNumber(self::MAXIMUM_USERNAME_LENGTH));
}
public static function validateUsername($username) {
// NOTE: If you update this, make sure to update:
//
// - Remarkup rule for @mentions.
// - Routing rule for "/p/username/".
// - Unit tests, obviously.
// - describeValidUsername() method, above.
if (strlen($username) > self::MAXIMUM_USERNAME_LENGTH) {
return false;
}
return (bool)preg_match('/^[a-zA-Z0-9._-]*[a-zA-Z0-9_-]\z/', $username);
}
public static function getDefaultProfileImageURI() {
return celerity_get_resource_uri('/rsrc/image/avatar.png');
}
public function getProfileImageURI() {
$uri_key = PhabricatorUserProfileImageCacheType::KEY_URI;
return $this->requireCacheData($uri_key);
}
public function getUnreadNotificationCount() {
$notification_key = PhabricatorUserNotificationCountCacheType::KEY_COUNT;
return $this->requireCacheData($notification_key);
}
public function getUnreadMessageCount() {
$message_key = PhabricatorUserMessageCountCacheType::KEY_COUNT;
return $this->requireCacheData($message_key);
}
public function getFullName() {
if (strlen($this->getRealName())) {
return $this->getUsername().' ('.$this->getRealName().')';
} else {
return $this->getUsername();
}
}
public function getTimeZone() {
return new DateTimeZone($this->getTimezoneIdentifier());
}
public function getTimeZoneOffset() {
$timezone = $this->getTimeZone();
$now = new DateTime('@'.PhabricatorTime::getNow());
$offset = $timezone->getOffset($now);
// Javascript offsets are in minutes and have the opposite sign.
$offset = -(int)($offset / 60);
return $offset;
}
public function getTimeZoneOffsetInHours() {
$offset = $this->getTimeZoneOffset();
$offset = (int)round($offset / 60);
$offset = -$offset;
return $offset;
}
public function formatShortDateTime($when, $now = null) {
if ($now === null) {
$now = PhabricatorTime::getNow();
}
try {
$when = new DateTime('@'.$when);
$now = new DateTime('@'.$now);
} catch (Exception $ex) {
return null;
}
$zone = $this->getTimeZone();
$when->setTimeZone($zone);
$now->setTimeZone($zone);
if ($when->format('Y') !== $now->format('Y')) {
// Different year, so show "Feb 31 2075".
$format = 'M j Y';
} else if ($when->format('Ymd') !== $now->format('Ymd')) {
// Same year but different month and day, so show "Feb 31".
$format = 'M j';
} else {
// Same year, month and day so show a time of day.
Continue modernizing application access to user preferences Summary: Ref T4103. This is just incremental cleanup: - Add "internal" settings, which aren't editable via the UI. They can still do validation and run through the normal pathway. Move a couple settings to use this. - Remove `getPreference()` on `PhabricatorUser`, which was a sort of prototype version of `getUserSetting()`. - Make `getUserSetting()` validate setting values before returning them, to improve robustness if we change allowable values later. - Add a user setting cache, since reading user settings was getting fairly expensive on Calendar. - Improve performance of setting validation for timezone setting (don't require building/computing all timezone offsets). - Since we have the cache anyway, make the timezone override a little more general in its approach. - Move editor stuff to use `getUserSetting()`. Test Plan: - Changed search scopes. - Reconciled local and server timezone settings by ignoring and changing timezones. - Changed date/time settings, browsed Calendar, queried date ranges. - Verified editor links generate properly in Diffusion. - Browsed around with time/date settings looking at timestamps. - Grepped for `getPreference()`, nuked all the ones coming off `$user` or `$viewer` that I could find. - Changed accessiblity to high-contrast colors. - Ran all unit tests. - Grepped for removed constants. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4103 Differential Revision: https://secure.phabricator.com/D16015
2016-06-02 17:12:15 -07:00
$pref_time = PhabricatorTimeFormatSetting::SETTINGKEY;
$format = $this->getUserSetting($pref_time);
}
return $when->format($format);
}
public function __toString() {
return $this->getUsername();
}
Allow users to have multiple email addresses, and verify emails Summary: - Move email to a separate table. - Migrate existing email to new storage. - Allow users to add and remove email addresses. - Allow users to verify email addresses. - Allow users to change their primary email address. - Convert all the registration/reset/login code to understand these changes. - There are a few security considerations here but I think I've addressed them. Principally, it is important to never let a user acquire a verified email address they don't actually own. We ensure this by tightening the scoping of token generation rules to be (user, email) specific. - This should have essentially zero impact on Facebook, but may require some minor changes in the registration code -- I don't exactly remember how it is set up. Not included here (next steps): - Allow configuration to restrict email to certain domains. - Allow configuration to require validated email. Test Plan: This is a fairly extensive, difficult-to-test change. - From "Email Addresses" interface: - Added new email (verified email verifications sent). - Changed primary email (verified old/new notificactions sent). - Resent verification emails (verified they sent). - Removed email. - Tried to add already-owned email. - Created new users with "accountadmin". Edited existing users with "accountadmin". - Created new users with "add_user.php". - Created new users with web interface. - Clicked welcome email link, verified it verified email. - Reset password. - Linked/unlinked oauth accounts. - Logged in with oauth account. - Logged in with email. - Registered with Oauth account. - Tried to register with OAuth account with duplicate email. - Verified errors for email verification with bad tokens, etc. Reviewers: btrahan, vrana, jungejason Reviewed By: btrahan CC: aran Maniphest Tasks: T1184 Differential Revision: https://secure.phabricator.com/D2393
2012-05-07 10:29:33 -07:00
public static function loadOneWithEmailAddress($address) {
$email = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$address);
if (!$email) {
return null;
}
return id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$email->getUserPHID());
}
public function getDefaultSpacePHID() {
// TODO: We might let the user switch which space they're "in" later on;
// for now just use the global space if one exists.
// If the viewer has access to the default space, use that.
$spaces = PhabricatorSpacesNamespaceQuery::getViewerActiveSpaces($this);
foreach ($spaces as $space) {
if ($space->getIsDefaultNamespace()) {
return $space->getPHID();
}
}
// Otherwise, use the space with the lowest ID that they have access to.
// This just tends to keep the default stable and predictable over time,
// so adding a new space won't change behavior for users.
if ($spaces) {
$spaces = msort($spaces, 'getID');
return head($spaces)->getPHID();
}
return null;
}
Improve Phortune policy behavior Summary: Currently, PhortuneAccounts have a very open default policy to allow merchants to see and interact with them. This has the undesirable side effect of leaking their names in too many places, because all users are allowed to load the handles for the accounts. Although this information is not super sensitive, we shouldn't expose it. I went through about 5 really messy diffs trying to fix this. It's very complicated because there are a lot of objects and many of them are related to PhortuneAccounts, but PhortuneAccounts are not bound to a specific merchant. This lead to a lot of threading viewers and merchants all over the place through the call stack and some really sketchy diffs with OmnipotentUsers that weren't going anywhere good. This is the cleanest approach I came up with, by far: - Introduce the concept of an "Authority", which gives a user more powers as a viewer. For now, since we only have one use case, this is pretty open-ended. - When a viewer is acting as a merchant, grant them authority through the merchant. - Have Accounts check if the viewer is acting with merchant authority. This lets us easily implement the rule "merchants can see this stuff" without being too broad. Then update the Subscription view to respect Merchant Authority. I partially updated the Cart views to respect it. I'll finish this up in a separate diff, but this seemed like a good checkpoint that introduced the concept without too much extra baggage. This feels pretty good/clean to me, overall, even ignoring the series of horrible messes I made on my way here. Test Plan: - Verified I can see everything I need to as a merchant (modulo un-updated Cart UIs). - Verified I can see nothing when acting as a normal user. Reviewers: btrahan Reviewed By: btrahan Subscribers: epriestley Differential Revision: https://secure.phabricator.com/D11945
2015-03-03 10:38:25 -08:00
/**
* Grant a user a source of authority, to let them bypass policy checks they
* could not otherwise.
*/
public function grantAuthority($authority) {
$this->authorities[] = $authority;
return $this;
}
/**
* Get authorities granted to the user.
*/
public function getAuthorities() {
return $this->authorities;
}
public function hasConduitClusterToken() {
return ($this->conduitClusterToken !== self::ATTACHABLE);
}
public function attachConduitClusterToken(PhabricatorConduitToken $token) {
$this->conduitClusterToken = $token;
return $this;
}
public function getConduitClusterToken() {
return $this->assertAttached($this->conduitClusterToken);
}
Improve Phortune policy behavior Summary: Currently, PhortuneAccounts have a very open default policy to allow merchants to see and interact with them. This has the undesirable side effect of leaking their names in too many places, because all users are allowed to load the handles for the accounts. Although this information is not super sensitive, we shouldn't expose it. I went through about 5 really messy diffs trying to fix this. It's very complicated because there are a lot of objects and many of them are related to PhortuneAccounts, but PhortuneAccounts are not bound to a specific merchant. This lead to a lot of threading viewers and merchants all over the place through the call stack and some really sketchy diffs with OmnipotentUsers that weren't going anywhere good. This is the cleanest approach I came up with, by far: - Introduce the concept of an "Authority", which gives a user more powers as a viewer. For now, since we only have one use case, this is pretty open-ended. - When a viewer is acting as a merchant, grant them authority through the merchant. - Have Accounts check if the viewer is acting with merchant authority. This lets us easily implement the rule "merchants can see this stuff" without being too broad. Then update the Subscription view to respect Merchant Authority. I partially updated the Cart views to respect it. I'll finish this up in a separate diff, but this seemed like a good checkpoint that introduced the concept without too much extra baggage. This feels pretty good/clean to me, overall, even ignoring the series of horrible messes I made on my way here. Test Plan: - Verified I can see everything I need to as a merchant (modulo un-updated Cart UIs). - Verified I can see nothing when acting as a normal user. Reviewers: btrahan Reviewed By: btrahan Subscribers: epriestley Differential Revision: https://secure.phabricator.com/D11945
2015-03-03 10:38:25 -08:00
/* -( Availability )------------------------------------------------------- */
/**
* @task availability
*/
public function attachAvailability(array $availability) {
$this->availability = $availability;
return $this;
}
/**
* Get the timestamp the user is away until, if they are currently away.
*
* @return int|null Epoch timestamp, or `null` if the user is not away.
* @task availability
*/
public function getAwayUntil() {
$availability = $this->availability;
$this->assertAttached($availability);
if (!$availability) {
return null;
}
return idx($availability, 'until');
}
public function getDisplayAvailability() {
$availability = $this->availability;
$this->assertAttached($availability);
if (!$availability) {
return null;
}
$busy = PhabricatorCalendarEventInvitee::AVAILABILITY_BUSY;
return idx($availability, 'availability', $busy);
}
public function getAvailabilityEventPHID() {
$availability = $this->availability;
$this->assertAttached($availability);
if (!$availability) {
return null;
}
return idx($availability, 'eventPHID');
}
/**
* Get cached availability, if present.
*
* @return wild|null Cache data, or null if no cache is available.
* @task availability
*/
public function getAvailabilityCache() {
$now = PhabricatorTime::getNow();
if ($this->availabilityCacheTTL <= $now) {
return null;
}
try {
return phutil_json_decode($this->availabilityCache);
} catch (Exception $ex) {
return null;
}
}
/**
* Write to the availability cache.
*
* @param wild Availability cache data.
* @param int|null Cache TTL.
* @return this
* @task availability
*/
public function writeAvailabilityCache(array $availability, $ttl) {
if (PhabricatorEnv::isReadOnly()) {
return $this;
}
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
queryfx(
$this->establishConnection('w'),
'UPDATE %T SET availabilityCache = %s, availabilityCacheTTL = %nd
WHERE id = %d',
$this->getTableName(),
json_encode($availability),
$ttl,
$this->getID());
unset($unguarded);
return $this;
}
/* -( Multi-Factor Authentication )---------------------------------------- */
/**
* Update the flag storing this user's enrollment in multi-factor auth.
*
* With certain settings, we need to check if a user has MFA on every page,
* so we cache MFA enrollment on the user object for performance. Calling this
* method synchronizes the cache by examining enrollment records. After
* updating the cache, use @{method:getIsEnrolledInMultiFactor} to check if
* the user is enrolled.
*
* This method should be called after any changes are made to a given user's
* multi-factor configuration.
*
* @return void
* @task factors
*/
public function updateMultiFactorEnrollment() {
$factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
$enrolled = count($factors) ? 1 : 0;
if ($enrolled !== $this->isEnrolledInMultiFactor) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
queryfx(
$this->establishConnection('w'),
'UPDATE %T SET isEnrolledInMultiFactor = %d WHERE id = %d',
$this->getTableName(),
$enrolled,
$this->getID());
unset($unguarded);
$this->isEnrolledInMultiFactor = $enrolled;
}
}
/**
* Check if the user is enrolled in multi-factor authentication.
*
* Enrolled users have one or more multi-factor authentication sources
* attached to their account. For performance, this value is cached. You
* can use @{method:updateMultiFactorEnrollment} to update the cache.
*
* @return bool True if the user is enrolled.
* @task factors
*/
public function getIsEnrolledInMultiFactor() {
return $this->isEnrolledInMultiFactor;
}
/* -( Omnipotence )-------------------------------------------------------- */
/**
* Returns true if this user is omnipotent. Omnipotent users bypass all policy
* checks.
*
* @return bool True if the user bypasses policy checks.
*/
public function isOmnipotent() {
return $this->omnipotent;
}
/**
* Get an omnipotent user object for use in contexts where there is no acting
* user, notably daemons.
*
* @return PhabricatorUser An omnipotent user.
*/
public static function getOmnipotentUser() {
static $user = null;
if (!$user) {
$user = new PhabricatorUser();
$user->omnipotent = true;
$user->makeEphemeral();
}
return $user;
}
/**
* Get a scalar string identifying this user.
*
* This is similar to using the PHID, but distinguishes between ominpotent
* and public users explicitly. This allows safe construction of cache keys
* or cache buckets which do not conflate public and omnipotent users.
*
* @return string Scalar identifier.
*/
public function getCacheFragment() {
if ($this->isOmnipotent()) {
return 'u.omnipotent';
}
$phid = $this->getPHID();
if ($phid) {
return 'u.'.$phid;
}
return 'u.public';
}
/* -( Managing Handles )--------------------------------------------------- */
/**
* Get a @{class:PhabricatorHandleList} which benefits from this viewer's
* internal handle pool.
*
* @param list<phid> List of PHIDs to load.
* @return PhabricatorHandleList Handle list object.
* @task handle
*/
public function loadHandles(array $phids) {
if ($this->handlePool === null) {
$this->handlePool = id(new PhabricatorHandlePool())
->setViewer($this);
}
return $this->handlePool->newHandleList($phids);
}
/**
* Get a @{class:PHUIHandleView} for a single handle.
*
* This benefits from the viewer's internal handle pool.
*
* @param phid PHID to render a handle for.
* @return PHUIHandleView View of the handle.
* @task handle
*/
public function renderHandle($phid) {
return $this->loadHandles(array($phid))->renderHandle($phid);
}
/**
* Get a @{class:PHUIHandleListView} for a list of handles.
*
* This benefits from the viewer's internal handle pool.
*
* @param list<phid> List of PHIDs to render.
* @return PHUIHandleListView View of the handles.
* @task handle
*/
public function renderHandleList(array $phids) {
return $this->loadHandles($phids)->renderList();
}
public function attachBadgePHIDs(array $phids) {
$this->badgePHIDs = $phids;
return $this;
}
public function getBadgePHIDs() {
return $this->assertAttached($this->badgePHIDs);
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::POLICY_PUBLIC;
case PhabricatorPolicyCapability::CAN_EDIT:
if ($this->getIsSystemAgent() || $this->getIsMailingList()) {
return PhabricatorPolicies::POLICY_ADMIN;
} else {
return PhabricatorPolicies::POLICY_NOONE;
}
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getPHID() && ($viewer->getPHID() === $this->getPHID());
}
public function describeAutomaticCapability($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_EDIT:
return pht('Only you can edit your information.');
default:
return null;
}
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return PhabricatorEnv::getEnvConfig('user.fields');
}
public function getCustomFieldBaseClass() {
Support configuration-driven custom fields Summary: Ref T1702. Ref T3718. There are a couple of things going on here: **PhabricatorCustomFieldList**: I added `PhabricatorCustomFieldList`, which is just a convenience class for dealing with lists of fields. Often, current field code does something like this inline in a Controller: foreach ($fields as $field) { // do some junk } Often, that junk has some slightly subtle implications. Move all of it to `$list->doSomeJunk()` methods (like `appendFieldsToForm()`, `loadFieldsFromStorage()`) to reduce code duplication and prevent errors. This additionally moves an existing list-convenience method there, out of `PhabricatorPropertyListView`. **PhabricatorUserConfiguredCustomFieldStorage**: Adds `PhabricatorUserConfiguredCustomFieldStorage` for storing custom field data (like "ICQ Handle", "Phone Number", "Desk", "Favorite Flower", etc). **Configuration-Driven Custom Fields**: Previously, I was thinking about doing these with interfaces, but as I thought about it more I started to dislike that approach. Instead, I built proxies into `PhabricatorCustomField`. Basically, this means that fields (like a custom, configuration-driven "Favorite Flower" field) can just use some other Field to actually provide their implementation (like a "standard" field which knows how to render text areas). The previous approach would have involed subclasssing the "standard" field and implementing an interface, but that would mean that every application would have at least two "base" fields and generally just seemed bleh as I worked through it. The cost of this approach is that we need a bunch of `proxy` junk in the base class, but that's a one-time cost and I think it simplifies all the implementations and makes them a lot less magical (e.g., all of the custom fields now extend the right base field classes). **Fixed Some Bugs**: Some of this code hadn't really been run yet and had minor bugs. Test Plan: {F54240} {F54241} {F54242} Reviewers: btrahan Reviewed By: btrahan CC: aran Maniphest Tasks: T1702, T1703, T3718 Differential Revision: https://secure.phabricator.com/D6749
2013-08-14 08:10:16 -07:00
return 'PhabricatorUserCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$externals = id(new PhabricatorExternalAccount())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
foreach ($externals as $external) {
$external->delete();
}
$prefs = id(new PhabricatorUserPreferencesQuery())
->setViewer($engine->getViewer())
->withUsers(array($this))
->execute();
foreach ($prefs as $pref) {
$engine->destroyObject($pref);
}
$profiles = id(new PhabricatorUserProfile())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
foreach ($profiles as $profile) {
$profile->delete();
}
Deactivate SSH keys instead of destroying them completely Summary: Ref T10917. Currently, when you delete an SSH key, we really truly delete it forever. This isn't very consistent with other applications, but we built this stuff a long time ago before we were as rigorous about retaining data and making it auditable. In partiular, destroying data isn't good for auditing after security issues, since it means we can't show you logs of any changes an attacker might have made to your keys. To prepare to improve this, stop destoying data. This will allow later changes to become transaction-oriented and show normal transaction logs. The tricky part here is that we have a `UNIQUE KEY` on the public key part of the key. Instead, I changed this to `UNIQUE (key, isActive)`, where `isActive` is a nullable boolean column. This works because MySQL does not enforce "unique" if part of the key is `NULL`. So you can't have two rows with `("A", 1)`, but you can have as many rows as you want with `("A", null)`. This lets us keep the "each key may only be active for one user/object" rule without requiring us to delete any data. Test Plan: - Ran schema changes. - Viewed public keys. - Tried to add a duplicate key, got rejected (already associated with another object). - Deleted SSH key. - Verified that the key was no longer actually deleted from the database, just marked inactive (in future changes, I'll update the UI to be more clear about this). - Uploaded a new copy of the same public key, worked fine (no duplicate key rejection). - Tried to upload yet another copy, got rejected. - Generated a new keypair. - Tried to upload a duplicate to an Almanac device, got rejected. - Generated a new pair for a device. - Trusted a device key. - Untrusted a device key. - "Deleted" a device key. - Tried to trust a deleted device key, got "inactive" message. - Ran `bin/ssh-auth`, got good output with unique keys. - Ran `cat ~/.ssh/id_rsa.pub | ./bin/ssh-auth-key`, got good output with one key. - Used `auth.querypublickeys` Conduit method to query keys, got good active keys. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10917 Differential Revision: https://secure.phabricator.com/D15943
2016-05-18 09:32:50 -07:00
$keys = id(new PhabricatorAuthSSHKeyQuery())
->setViewer($engine->getViewer())
->withObjectPHIDs(array($this->getPHID()))
->execute();
foreach ($keys as $key) {
Deactivate SSH keys instead of destroying them completely Summary: Ref T10917. Currently, when you delete an SSH key, we really truly delete it forever. This isn't very consistent with other applications, but we built this stuff a long time ago before we were as rigorous about retaining data and making it auditable. In partiular, destroying data isn't good for auditing after security issues, since it means we can't show you logs of any changes an attacker might have made to your keys. To prepare to improve this, stop destoying data. This will allow later changes to become transaction-oriented and show normal transaction logs. The tricky part here is that we have a `UNIQUE KEY` on the public key part of the key. Instead, I changed this to `UNIQUE (key, isActive)`, where `isActive` is a nullable boolean column. This works because MySQL does not enforce "unique" if part of the key is `NULL`. So you can't have two rows with `("A", 1)`, but you can have as many rows as you want with `("A", null)`. This lets us keep the "each key may only be active for one user/object" rule without requiring us to delete any data. Test Plan: - Ran schema changes. - Viewed public keys. - Tried to add a duplicate key, got rejected (already associated with another object). - Deleted SSH key. - Verified that the key was no longer actually deleted from the database, just marked inactive (in future changes, I'll update the UI to be more clear about this). - Uploaded a new copy of the same public key, worked fine (no duplicate key rejection). - Tried to upload yet another copy, got rejected. - Generated a new keypair. - Tried to upload a duplicate to an Almanac device, got rejected. - Generated a new pair for a device. - Trusted a device key. - Untrusted a device key. - "Deleted" a device key. - Tried to trust a deleted device key, got "inactive" message. - Ran `bin/ssh-auth`, got good output with unique keys. - Ran `cat ~/.ssh/id_rsa.pub | ./bin/ssh-auth-key`, got good output with one key. - Used `auth.querypublickeys` Conduit method to query keys, got good active keys. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10917 Differential Revision: https://secure.phabricator.com/D15943
2016-05-18 09:32:50 -07:00
$engine->destroyObject($key);
}
$emails = id(new PhabricatorUserEmail())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
foreach ($emails as $email) {
$email->delete();
}
$sessions = id(new PhabricatorAuthSession())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
foreach ($sessions as $session) {
$session->delete();
}
$factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
foreach ($factors as $factor) {
$factor->delete();
}
$this->saveTransaction();
}
/* -( PhabricatorSSHPublicKeyInterface )----------------------------------- */
public function getSSHPublicKeyManagementURI(PhabricatorUser $viewer) {
if ($viewer->getPHID() == $this->getPHID()) {
// If the viewer is managing their own keys, take them to the normal
// panel.
return '/settings/panel/ssh/';
} else {
// Otherwise, take them to the administrative panel for this user.
return '/settings/'.$this->getID().'/panel/ssh/';
}
}
public function getSSHKeyDefaultName() {
return 'id_rsa_phabricator';
}
public function getSSHKeyNotifyPHIDs() {
return array(
$this->getPHID(),
);
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorUserProfileEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorUserTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new PhabricatorUserFulltextEngine();
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('username')
->setType('string')
->setDescription(pht("The user's username.")),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('realName')
->setType('string')
->setDescription(pht("The user's real name.")),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('roles')
->setType('list<string>')
->setDescription(pht('List of acccount roles.')),
);
}
public function getFieldValuesForConduit() {
$roles = array();
if ($this->getIsDisabled()) {
$roles[] = 'disabled';
}
if ($this->getIsSystemAgent()) {
$roles[] = 'bot';
}
if ($this->getIsMailingList()) {
$roles[] = 'list';
}
if ($this->getIsAdmin()) {
$roles[] = 'admin';
}
if ($this->getIsEmailVerified()) {
$roles[] = 'verified';
}
if ($this->getIsApproved()) {
$roles[] = 'approved';
}
if ($this->isUserActivated()) {
$roles[] = 'activated';
}
return array(
'username' => $this->getUsername(),
'realName' => $this->getRealName(),
'roles' => $roles,
);
}
public function getConduitSearchAttachments() {
return array();
}
Provide a general-purpose, modular user cache for settings and other similar data Summary: Ref T4103. Currently, we issue a `SELECT * FROM user_preferences ... WHERE userPHID = ...` on every page to load the viewer's settings. There are several other questionable data accesses on every page too, most of which could benefit from improved caching strategies (see T4103#178122). This query will soon get more expensive, since it may need to load several objects (e.g., the user's settings and their "role profile" settings). Although we could put that data on the User and do both in one query, it's nicer to put it on the Preferences object ("This inherits from profile X") which means we need to do several queries. Rather than paying a greater price, we can cheat this stuff into the existing query where we load the user's session by providing a user cache table and doing some JOIN magic. This lets us issue one query and try to get cache hits on a bunch of caches cheaply (well, we'll be in trouble at the MySQL JOIN limit of 61 tables, but have some headroom). For now, just get it working: - Add the table. - Try to get user settings "for free" when we load the session. - If we miss, fill user settings into the cache on-demand. - We only use this in one place (DarkConsole) for now. I'll use it more widely in the next diff. Test Plan: - Loaded page as logged-in user. - Loaded page as logged-out user. - Examined session query to see cache joins. - Changed settings, saw database cache fill. - Toggled DarkConsole on and off. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4103 Differential Revision: https://secure.phabricator.com/D16001
2016-06-01 12:14:39 -07:00
/* -( User Cache )--------------------------------------------------------- */
/**
* @task cache
*/
public function attachRawCacheData(array $data) {
$this->rawCacheData = $data + $this->rawCacheData;
return $this;
}
public function setAllowInlineCacheGeneration($allow_cache_generation) {
$this->allowInlineCacheGeneration = $allow_cache_generation;
return $this;
}
Provide a general-purpose, modular user cache for settings and other similar data Summary: Ref T4103. Currently, we issue a `SELECT * FROM user_preferences ... WHERE userPHID = ...` on every page to load the viewer's settings. There are several other questionable data accesses on every page too, most of which could benefit from improved caching strategies (see T4103#178122). This query will soon get more expensive, since it may need to load several objects (e.g., the user's settings and their "role profile" settings). Although we could put that data on the User and do both in one query, it's nicer to put it on the Preferences object ("This inherits from profile X") which means we need to do several queries. Rather than paying a greater price, we can cheat this stuff into the existing query where we load the user's session by providing a user cache table and doing some JOIN magic. This lets us issue one query and try to get cache hits on a bunch of caches cheaply (well, we'll be in trouble at the MySQL JOIN limit of 61 tables, but have some headroom). For now, just get it working: - Add the table. - Try to get user settings "for free" when we load the session. - If we miss, fill user settings into the cache on-demand. - We only use this in one place (DarkConsole) for now. I'll use it more widely in the next diff. Test Plan: - Loaded page as logged-in user. - Loaded page as logged-out user. - Examined session query to see cache joins. - Changed settings, saw database cache fill. - Toggled DarkConsole on and off. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4103 Differential Revision: https://secure.phabricator.com/D16001
2016-06-01 12:14:39 -07:00
/**
* @task cache
*/
protected function requireCacheData($key) {
if (isset($this->usableCacheData[$key])) {
return $this->usableCacheData[$key];
}
$type = PhabricatorUserCacheType::requireCacheTypeForKey($key);
if (isset($this->rawCacheData[$key])) {
$raw_value = $this->rawCacheData[$key];
$usable_value = $type->getValueFromStorage($raw_value);
$this->usableCacheData[$key] = $usable_value;
return $usable_value;
}
// By default, we throw if a cache isn't available. This is consistent
// with the standard `needX()` + `attachX()` + `getX()` interaction.
if (!$this->allowInlineCacheGeneration) {
throw new PhabricatorDataNotAttachedException($this);
}
$user_phid = $this->getPHID();
// Try to read the actual cache before we generate a new value. We can
// end up here via Conduit, which does not use normal sessions and can
// not pick up a free cache load during session identification.
if ($user_phid) {
$raw_data = PhabricatorUserCache::readCaches(
$type,
$key,
array($user_phid));
if (array_key_exists($user_phid, $raw_data)) {
$raw_value = $raw_data[$user_phid];
$usable_value = $type->getValueFromStorage($raw_value);
$this->rawCacheData[$key] = $raw_value;
$this->usableCacheData[$key] = $usable_value;
return $usable_value;
}
}
Provide a general-purpose, modular user cache for settings and other similar data Summary: Ref T4103. Currently, we issue a `SELECT * FROM user_preferences ... WHERE userPHID = ...` on every page to load the viewer's settings. There are several other questionable data accesses on every page too, most of which could benefit from improved caching strategies (see T4103#178122). This query will soon get more expensive, since it may need to load several objects (e.g., the user's settings and their "role profile" settings). Although we could put that data on the User and do both in one query, it's nicer to put it on the Preferences object ("This inherits from profile X") which means we need to do several queries. Rather than paying a greater price, we can cheat this stuff into the existing query where we load the user's session by providing a user cache table and doing some JOIN magic. This lets us issue one query and try to get cache hits on a bunch of caches cheaply (well, we'll be in trouble at the MySQL JOIN limit of 61 tables, but have some headroom). For now, just get it working: - Add the table. - Try to get user settings "for free" when we load the session. - If we miss, fill user settings into the cache on-demand. - We only use this in one place (DarkConsole) for now. I'll use it more widely in the next diff. Test Plan: - Loaded page as logged-in user. - Loaded page as logged-out user. - Examined session query to see cache joins. - Changed settings, saw database cache fill. - Toggled DarkConsole on and off. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4103 Differential Revision: https://secure.phabricator.com/D16001
2016-06-01 12:14:39 -07:00
$usable_value = $type->getDefaultValue();
if ($user_phid) {
$map = $type->newValueForUsers($key, array($this));
if (array_key_exists($user_phid, $map)) {
$raw_value = $map[$user_phid];
$usable_value = $type->getValueFromStorage($raw_value);
Provide a general-purpose, modular user cache for settings and other similar data Summary: Ref T4103. Currently, we issue a `SELECT * FROM user_preferences ... WHERE userPHID = ...` on every page to load the viewer's settings. There are several other questionable data accesses on every page too, most of which could benefit from improved caching strategies (see T4103#178122). This query will soon get more expensive, since it may need to load several objects (e.g., the user's settings and their "role profile" settings). Although we could put that data on the User and do both in one query, it's nicer to put it on the Preferences object ("This inherits from profile X") which means we need to do several queries. Rather than paying a greater price, we can cheat this stuff into the existing query where we load the user's session by providing a user cache table and doing some JOIN magic. This lets us issue one query and try to get cache hits on a bunch of caches cheaply (well, we'll be in trouble at the MySQL JOIN limit of 61 tables, but have some headroom). For now, just get it working: - Add the table. - Try to get user settings "for free" when we load the session. - If we miss, fill user settings into the cache on-demand. - We only use this in one place (DarkConsole) for now. I'll use it more widely in the next diff. Test Plan: - Loaded page as logged-in user. - Loaded page as logged-out user. - Examined session query to see cache joins. - Changed settings, saw database cache fill. - Toggled DarkConsole on and off. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4103 Differential Revision: https://secure.phabricator.com/D16001
2016-06-01 12:14:39 -07:00
$this->rawCacheData[$key] = $raw_value;
PhabricatorUserCache::writeCache(
$type,
$key,
$user_phid,
$raw_value);
}
}
$this->usableCacheData[$key] = $usable_value;
return $usable_value;
}
/**
* @task cache
*/
public function clearCacheData($key) {
unset($this->rawCacheData[$key]);
unset($this->usableCacheData[$key]);
return $this;
}
2011-01-23 18:09:16 -08:00
}