Path: blob/master/src/applications/auth/provider/PhabricatorAuthProvider.php
12256 views
<?php12abstract class PhabricatorAuthProvider extends Phobject {34private $providerConfig;56public function attachProviderConfig(PhabricatorAuthProviderConfig $config) {7$this->providerConfig = $config;8return $this;9}1011public function hasProviderConfig() {12return (bool)$this->providerConfig;13}1415public function getProviderConfig() {16if ($this->providerConfig === null) {17throw new PhutilInvalidStateException('attachProviderConfig');18}19return $this->providerConfig;20}2122public function getProviderConfigPHID() {23return $this->getProviderConfig()->getPHID();24}2526public function getConfigurationHelp() {27return null;28}2930public function getDefaultProviderConfig() {31return id(new PhabricatorAuthProviderConfig())32->setProviderClass(get_class($this))33->setIsEnabled(1)34->setShouldAllowLogin(1)35->setShouldAllowRegistration(1)36->setShouldAllowLink(1)37->setShouldAllowUnlink(1);38}3940public function getNameForCreate() {41return $this->getProviderName();42}4344public function getDescriptionForCreate() {45return null;46}4748public function getProviderKey() {49return $this->getAdapter()->getAdapterKey();50}5152public function getProviderType() {53return $this->getAdapter()->getAdapterType();54}5556public function getProviderDomain() {57return $this->getAdapter()->getAdapterDomain();58}5960public static function getAllBaseProviders() {61return id(new PhutilClassMapQuery())62->setAncestorClass(__CLASS__)63->execute();64}6566public static function getAllProviders() {67static $providers;6869if ($providers === null) {70$objects = self::getAllBaseProviders();7172$configs = id(new PhabricatorAuthProviderConfigQuery())73->setViewer(PhabricatorUser::getOmnipotentUser())74->execute();7576$providers = array();77foreach ($configs as $config) {78if (!isset($objects[$config->getProviderClass()])) {79// This configuration is for a provider which is not installed.80continue;81}8283$object = clone $objects[$config->getProviderClass()];84$object->attachProviderConfig($config);8586$key = $object->getProviderKey();87if (isset($providers[$key])) {88throw new Exception(89pht(90"Two authentication providers use the same provider key ".91"('%s'). Each provider must be identified by a unique key.",92$key));93}94$providers[$key] = $object;95}96}9798return $providers;99}100101public static function getAllEnabledProviders() {102$providers = self::getAllProviders();103foreach ($providers as $key => $provider) {104if (!$provider->isEnabled()) {105unset($providers[$key]);106}107}108return $providers;109}110111public static function getEnabledProviderByKey($provider_key) {112return idx(self::getAllEnabledProviders(), $provider_key);113}114115abstract public function getProviderName();116abstract public function getAdapter();117118public function isEnabled() {119return $this->getProviderConfig()->getIsEnabled();120}121122public function shouldAllowLogin() {123return $this->getProviderConfig()->getShouldAllowLogin();124}125126public function shouldAllowRegistration() {127if (!$this->shouldAllowLogin()) {128return false;129}130131return $this->getProviderConfig()->getShouldAllowRegistration();132}133134public function shouldAllowAccountLink() {135return $this->getProviderConfig()->getShouldAllowLink();136}137138public function shouldAllowAccountUnlink() {139return $this->getProviderConfig()->getShouldAllowUnlink();140}141142public function shouldTrustEmails() {143return $this->shouldAllowEmailTrustConfiguration() &&144$this->getProviderConfig()->getShouldTrustEmails();145}146147/**148* Should we allow the adapter to be marked as "trusted". This is true for149* all adapters except those that allow the user to type in emails (see150* @{class:PhabricatorPasswordAuthProvider}).151*/152public function shouldAllowEmailTrustConfiguration() {153return true;154}155156public function buildLoginForm(PhabricatorAuthStartController $controller) {157return $this->renderLoginForm($controller->getRequest(), $mode = 'start');158}159160public function buildInviteForm(PhabricatorAuthStartController $controller) {161return $this->renderLoginForm($controller->getRequest(), $mode = 'invite');162}163164abstract public function processLoginRequest(165PhabricatorAuthLoginController $controller);166167public function buildLinkForm($controller) {168return $this->renderLoginForm($controller->getRequest(), $mode = 'link');169}170171public function shouldAllowAccountRefresh() {172return true;173}174175public function buildRefreshForm(176PhabricatorAuthLinkController $controller) {177return $this->renderLoginForm($controller->getRequest(), $mode = 'refresh');178}179180protected function renderLoginForm(AphrontRequest $request, $mode) {181throw new PhutilMethodNotImplementedException();182}183184public function createProviders() {185return array($this);186}187188protected function willSaveAccount(PhabricatorExternalAccount $account) {189return;190}191192final protected function newExternalAccountForIdentifiers(193array $identifiers) {194195assert_instances_of($identifiers, 'PhabricatorExternalAccountIdentifier');196197if (!$identifiers) {198throw new Exception(199pht(200'Authentication provider (of class "%s") is attempting to '.201'load or create an external account, but provided no account '.202'identifiers.',203get_class($this)));204}205206$config = $this->getProviderConfig();207$viewer = PhabricatorUser::getOmnipotentUser();208209$raw_identifiers = mpull($identifiers, 'getIdentifierRaw');210211$accounts = id(new PhabricatorExternalAccountQuery())212->setViewer($viewer)213->withProviderConfigPHIDs(array($config->getPHID()))214->withRawAccountIdentifiers($raw_identifiers)215->needAccountIdentifiers(true)216->execute();217if (!$accounts) {218$account = $this->newExternalAccount();219} else if (count($accounts) === 1) {220$account = head($accounts);221} else {222throw new Exception(223pht(224'Authentication provider (of class "%s") is attempting to load '.225'or create an external account, but provided a list of '.226'account identifiers which map to more than one account: %s.',227get_class($this),228implode(', ', $raw_identifiers)));229}230231// See T13493. Add all the identifiers to the account. In the case where232// an account initially has a lower-quality identifier (like an email233// address) and later adds a higher-quality identifier (like a GUID), this234// allows us to automatically upgrade toward the higher-quality identifier235// and survive API changes which remove the lower-quality identifier more236// gracefully.237238foreach ($identifiers as $identifier) {239$account->appendIdentifier($identifier);240}241242return $this->didUpdateAccount($account);243}244245final protected function newExternalAccountForUser(PhabricatorUser $user) {246$config = $this->getProviderConfig();247248// When a user logs in with a provider like username/password, they249// always already have a Phabricator account (since there's no way they250// could have a username otherwise).251252// These users should never go to registration, so we're building a253// dummy "external account" which just links directly back to their254// internal account.255256$account = id(new PhabricatorExternalAccountQuery())257->setViewer($user)258->withProviderConfigPHIDs(array($config->getPHID()))259->withUserPHIDs(array($user->getPHID()))260->executeOne();261if (!$account) {262$account = $this->newExternalAccount()263->setUserPHID($user->getPHID());264}265266return $this->didUpdateAccount($account);267}268269private function didUpdateAccount(PhabricatorExternalAccount $account) {270$adapter = $this->getAdapter();271272$account->setUsername($adapter->getAccountName());273$account->setRealName($adapter->getAccountRealName());274$account->setEmail($adapter->getAccountEmail());275$account->setAccountURI($adapter->getAccountURI());276277$account->setProfileImagePHID(null);278$image_uri = $adapter->getAccountImageURI();279if ($image_uri) {280try {281$name = PhabricatorSlug::normalize($this->getProviderName());282$name = $name.'-profile.jpg';283284// TODO: If the image has not changed, we do not need to make a new285// file entry for it, but there's no convenient way to do this with286// PhabricatorFile right now. The storage will get shared, so the impact287// here is negligible.288289$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();290$image_file = PhabricatorFile::newFromFileDownload(291$image_uri,292array(293'name' => $name,294'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,295));296if ($image_file->isViewableImage()) {297$image_file298->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy())299->setCanCDN(true)300->save();301$account->setProfileImagePHID($image_file->getPHID());302} else {303$image_file->delete();304}305unset($unguarded);306307} catch (Exception $ex) {308// Log this but proceed, it's not especially important that we309// be able to pull profile images.310phlog($ex);311}312}313314$this->willSaveAccount($account);315316$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();317$account->save();318unset($unguarded);319320return $account;321}322323public function getLoginURI() {324$app = PhabricatorApplication::getByClass('PhabricatorAuthApplication');325return $app->getApplicationURI('/login/'.$this->getProviderKey().'/');326}327328public function getSettingsURI() {329return '/settings/panel/external/';330}331332public function getStartURI() {333$app = PhabricatorApplication::getByClass('PhabricatorAuthApplication');334$uri = $app->getApplicationURI('/start/');335return $uri;336}337338public function isDefaultRegistrationProvider() {339return false;340}341342public function shouldRequireRegistrationPassword() {343return false;344}345346public function newDefaultExternalAccount() {347return $this->newExternalAccount();348}349350protected function newExternalAccount() {351$config = $this->getProviderConfig();352$adapter = $this->getAdapter();353354$account = id(new PhabricatorExternalAccount())355->setProviderConfigPHID($config->getPHID())356->attachAccountIdentifiers(array());357358// TODO: Remove this when these columns are removed. They no longer have359// readers or writers (other than this callsite).360361$account362->setAccountType($adapter->getAdapterType())363->setAccountDomain($adapter->getAdapterDomain());364365// TODO: Remove this when "accountID" is removed; the column is not366// nullable.367368$account->setAccountID('');369370return $account;371}372373public function getLoginOrder() {374return '500-'.$this->getProviderName();375}376377protected function getLoginIcon() {378return 'Generic';379}380381public function newIconView() {382return id(new PHUIIconView())383->setSpriteSheet(PHUIIconView::SPRITE_LOGIN)384->setSpriteIcon($this->getLoginIcon());385}386387public function isLoginFormAButton() {388return false;389}390391public function renderConfigPropertyTransactionTitle(392PhabricatorAuthProviderConfigTransaction $xaction) {393394return null;395}396397public function readFormValuesFromProvider() {398return array();399}400401public function readFormValuesFromRequest(AphrontRequest $request) {402return array();403}404405public function processEditForm(406AphrontRequest $request,407array $values) {408409$errors = array();410$issues = array();411412return array($errors, $issues, $values);413}414415public function extendEditForm(416AphrontRequest $request,417AphrontFormView $form,418array $values,419array $issues) {420421return;422}423424public function willRenderLinkedAccount(425PhabricatorUser $viewer,426PHUIObjectItemView $item,427PhabricatorExternalAccount $account) {428429$account_view = id(new PhabricatorAuthAccountView())430->setExternalAccount($account)431->setAuthProvider($this);432433$item->appendChild(434phutil_tag(435'div',436array(437'class' => 'mmr mml mst mmb',438),439$account_view));440}441442/**443* Return true to use a two-step configuration (setup, configure) instead of444* the default single-step configuration. In practice, this means that445* creating a new provider instance will redirect back to the edit page446* instead of the provider list.447*448* @return bool True if this provider uses two-step configuration.449*/450public function hasSetupStep() {451return false;452}453454/**455* Render a standard login/register button element.456*457* The `$attributes` parameter takes these keys:458*459* - `uri`: URI the button should take the user to when clicked.460* - `method`: Optional HTTP method the button should use, defaults to GET.461*462* @param AphrontRequest HTTP request.463* @param string Request mode string.464* @param map Additional parameters, see above.465* @return wild Log in button.466*/467protected function renderStandardLoginButton(468AphrontRequest $request,469$mode,470array $attributes = array()) {471472PhutilTypeSpec::checkMap(473$attributes,474array(475'method' => 'optional string',476'uri' => 'string',477'sigil' => 'optional string',478));479480$viewer = $request->getUser();481$adapter = $this->getAdapter();482483if ($mode == 'link') {484$button_text = pht('Link External Account');485} else if ($mode == 'refresh') {486$button_text = pht('Refresh Account Link');487} else if ($mode == 'invite') {488$button_text = pht('Register Account');489} else if ($this->shouldAllowRegistration()) {490$button_text = pht('Log In or Register');491} else {492$button_text = pht('Log In');493}494495$icon = id(new PHUIIconView())496->setSpriteSheet(PHUIIconView::SPRITE_LOGIN)497->setSpriteIcon($this->getLoginIcon());498499$button = id(new PHUIButtonView())500->setSize(PHUIButtonView::BIG)501->setColor(PHUIButtonView::GREY)502->setIcon($icon)503->setText($button_text)504->setSubtext($this->getProviderName());505506$uri = $attributes['uri'];507$uri = new PhutilURI($uri);508$params = $uri->getQueryParamsAsPairList();509$uri->removeAllQueryParams();510511$content = array($button);512513foreach ($params as $pair) {514list($key, $value) = $pair;515$content[] = phutil_tag(516'input',517array(518'type' => 'hidden',519'name' => $key,520'value' => $value,521));522}523524$static_response = CelerityAPI::getStaticResourceResponse();525$static_response->addContentSecurityPolicyURI('form-action', (string)$uri);526527foreach ($this->getContentSecurityPolicyFormActions() as $csp_uri) {528$static_response->addContentSecurityPolicyURI('form-action', $csp_uri);529}530531return phabricator_form(532$viewer,533array(534'method' => idx($attributes, 'method', 'GET'),535'action' => (string)$uri,536'sigil' => idx($attributes, 'sigil'),537),538$content);539}540541public function renderConfigurationFooter() {542return null;543}544545public function getAuthCSRFCode(AphrontRequest $request) {546$phcid = $request->getCookie(PhabricatorCookies::COOKIE_CLIENTID);547if (!strlen($phcid)) {548throw new AphrontMalformedRequestException(549pht('Missing Client ID Cookie'),550pht(551'Your browser did not submit a "%s" cookie with client state '.552'information in the request. Check that cookies are enabled. '.553'If this problem persists, you may need to clear your cookies.',554PhabricatorCookies::COOKIE_CLIENTID),555true);556}557558return PhabricatorHash::weakDigest($phcid);559}560561protected function verifyAuthCSRFCode(AphrontRequest $request, $actual) {562$expect = $this->getAuthCSRFCode($request);563564if (!strlen($actual)) {565throw new Exception(566pht(567'The authentication provider did not return a client state '.568'parameter in its response, but one was expected. If this '.569'problem persists, you may need to clear your cookies.'));570}571572if (!phutil_hashes_are_identical($actual, $expect)) {573throw new Exception(574pht(575'The authentication provider did not return the correct client '.576'state parameter in its response. If this problem persists, you may '.577'need to clear your cookies.'));578}579}580581public function supportsAutoLogin() {582return false;583}584585public function getAutoLoginURI(AphrontRequest $request) {586throw new PhutilMethodNotImplementedException();587}588589protected function getContentSecurityPolicyFormActions() {590return array();591}592593}594595596