diff --git a/resources/sql/autopatches/20150205.authprovider.autologin.sql b/resources/sql/autopatches/20150205.authprovider.autologin.sql new file mode 100644 index 000000000..7fb0bbf25 --- /dev/null +++ b/resources/sql/autopatches/20150205.authprovider.autologin.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_auth.auth_providerconfig + ADD shouldAutoLogin TINYINT(1) NOT NULL DEFAULT '0'; diff --git a/src/applications/auth/application/PhabricatorAuthApplication.php b/src/applications/auth/application/PhabricatorAuthApplication.php index 70d1966fc..181f7986b 100644 --- a/src/applications/auth/application/PhabricatorAuthApplication.php +++ b/src/applications/auth/application/PhabricatorAuthApplication.php @@ -1,154 +1,155 @@ getIsAdmin(); } public function getName() { return pht('Auth'); } public function getShortDescription() { return pht('Login/Registration'); } public function getHelpURI() { // NOTE: Although reasonable help exists for this in "Configuring Accounts // and Registration", specifying a help URI here means we get the menu // item in all the login/link interfaces, which is confusing and not // helpful. // TODO: Special case this, or split the auth and auth administration // applications? return null; } public function buildMainMenuItems( PhabricatorUser $user, PhabricatorController $controller = null) { $items = array(); if ($user->isLoggedIn()) { $item = id(new PHUIListItemView()) ->addClass('core-menu-item') ->setName(pht('Log Out')) ->setIcon('fa-sign-out') ->setWorkflow(true) ->setHref('/logout/') ->setSelected(($controller instanceof PhabricatorLogoutController)) ->setAural(pht('Log Out')) ->setOrder(900); $items[] = $item; } else { if ($controller instanceof PhabricatorAuthController) { // Don't show the "Login" item on auth controllers, since they're // generally all related to logging in anyway. } else { $uri = new PhutilURI('/auth/start/'); if ($controller) { $path = $controller->getRequest()->getPath(); $uri->setQueryParam('next', $path); } $item = id(new PHUIListItemView()) ->addClass('core-menu-item') ->setName(pht('Log In')) // TODO: Login icon? ->setIcon('fa-sign-in') ->setHref($uri) ->setAural(pht('Log In')) ->setOrder(900); $items[] = $item; } } return $items; } public function getApplicationGroup() { return self::GROUP_ADMIN; } public function getRoutes() { return array( '/auth/' => array( '' => 'PhabricatorAuthListController', 'config/' => array( 'new/' => 'PhabricatorAuthNewController', 'new/(?P[^/]+)/' => 'PhabricatorAuthEditController', 'edit/(?P\d+)/' => 'PhabricatorAuthEditController', '(?Penable|disable)/(?P\d+)/' => 'PhabricatorAuthDisableController', ), 'login/(?P[^/]+)/(?:(?P[^/]+)/)?' => 'PhabricatorAuthLoginController', + '(?Ploggedout)/' => 'PhabricatorAuthStartController', 'register/(?:(?P[^/]+)/)?' => 'PhabricatorAuthRegisterController', 'start/' => 'PhabricatorAuthStartController', 'validate/' => 'PhabricatorAuthValidateController', 'finish/' => 'PhabricatorAuthFinishController', 'unlink/(?P[^/]+)/' => 'PhabricatorAuthUnlinkController', '(?Plink|refresh)/(?P[^/]+)/' => 'PhabricatorAuthLinkController', 'confirmlink/(?P[^/]+)/' => 'PhabricatorAuthConfirmLinkController', 'session/terminate/(?P[^/]+)/' => 'PhabricatorAuthTerminateSessionController', 'token/revoke/(?P[^/]+)/' => 'PhabricatorAuthRevokeTokenController', 'session/downgrade/' => 'PhabricatorAuthDowngradeSessionController', 'multifactor/' => 'PhabricatorAuthNeedsMultiFactorController', 'sshkey/' => array( 'generate/' => 'PhabricatorAuthSSHKeyGenerateController', 'upload/' => 'PhabricatorAuthSSHKeyEditController', 'edit/(?P\d+)/' => 'PhabricatorAuthSSHKeyEditController', 'delete/(?P\d+)/' => 'PhabricatorAuthSSHKeyDeleteController', ), ), '/oauth/(?P\w+)/login/' => 'PhabricatorAuthOldOAuthRedirectController', '/login/' => array( '' => 'PhabricatorAuthStartController', 'email/' => 'PhabricatorEmailLoginController', 'once/'. '(?P[^/]+)/'. '(?P\d+)/'. '(?P[^/]+)/'. '(?:(?P\d+)/)?' => 'PhabricatorAuthOneTimeLoginController', 'refresh/' => 'PhabricatorRefreshCSRFController', 'mustverify/' => 'PhabricatorMustVerifyEmailController', ), '/emailverify/(?P[^/]+)/' => 'PhabricatorEmailVerificationController', '/logout/' => 'PhabricatorLogoutController', ); } protected function getCustomCapabilities() { return array( AuthManageProvidersCapability::CAPABILITY => array( 'default' => PhabricatorPolicies::POLICY_ADMIN, ), ); } } diff --git a/src/applications/auth/controller/PhabricatorAuthStartController.php b/src/applications/auth/controller/PhabricatorAuthStartController.php index c2b8c918a..34ea44030 100644 --- a/src/applications/auth/controller/PhabricatorAuthStartController.php +++ b/src/applications/auth/controller/PhabricatorAuthStartController.php @@ -1,218 +1,230 @@ getRequest(); + public function handleRequest(AphrontRequest $request) { $viewer = $request->getUser(); if ($viewer->isLoggedIn()) { // Kick the user home if they are already logged in. return id(new AphrontRedirectResponse())->setURI('/'); } if ($request->isAjax()) { return $this->processAjaxRequest(); } if ($request->isConduit()) { return $this->processConduitRequest(); } // If the user gets this far, they aren't logged in, so if they have a // user session token we can conclude that it's invalid: if it was valid, // they'd have been logged in above and never made it here. Try to clear // it and warn the user they may need to nuke their cookies. $session_token = $request->getCookie(PhabricatorCookies::COOKIE_SESSION); if (strlen($session_token)) { $kind = PhabricatorAuthSessionEngine::getSessionKindFromToken( $session_token); switch ($kind) { case PhabricatorAuthSessionEngine::KIND_ANONYMOUS: // If this is an anonymous session. It's expected that they won't // be logged in, so we can just continue. break; default: // The session cookie is invalid, so clear it. $request->clearCookie(PhabricatorCookies::COOKIE_USERNAME); $request->clearCookie(PhabricatorCookies::COOKIE_SESSION); return $this->renderError( pht( 'Your login session is invalid. Try reloading the page and '. 'logging in again. If that does not work, clear your browser '. 'cookies.')); } } $providers = PhabricatorAuthProvider::getAllEnabledProviders(); foreach ($providers as $key => $provider) { if (!$provider->shouldAllowLogin()) { unset($providers[$key]); } } if (!$providers) { if ($this->isFirstTimeSetup()) { // If this is a fresh install, let the user register their admin // account. return id(new AphrontRedirectResponse()) ->setURI($this->getApplicationURI('/register/')); } return $this->renderError( pht( 'This Phabricator install is not configured with any enabled '. 'authentication providers which can be used to log in. If you '. 'have accidentally locked yourself out by disabling all providers, '. 'you can use `phabricator/bin/auth recover ` to '. 'recover access to an administrative account.')); } $next_uri = $request->getStr('next'); if (!strlen($next_uri)) { if ($this->getDelegatingController()) { // Only set a next URI from the request path if this controller was // delegated to, which happens when a user tries to view a page which // requires them to login. // If this controller handled the request directly, we're on the main // login page, and never want to redirect the user back here after they // login. $next_uri = (string)$this->getRequest()->getRequestURI(); } } if (!$request->isFormPost()) { if (strlen($next_uri)) { PhabricatorCookies::setNextURICookie($request, $next_uri); } PhabricatorCookies::setClientIDCookie($request); } + if (!$request->getURIData('loggedout') && count($providers) == 1) { + $auto_login_provider = head($providers); + $auto_login_config = $auto_login_provider->getProviderConfig(); + if ($auto_login_provider instanceof PhabricatorPhabricatorAuthProvider && + $auto_login_config->getShouldAutoLogin()) { + $auto_login_adapter = $provider->getAdapter(); + $auto_login_adapter->setState($provider->getAuthCSRFCode($request)); + return id(new AphrontRedirectResponse()) + ->setIsExternal(true) + ->setURI($provider->getAdapter()->getAuthenticateURI()); + } + } + $not_buttons = array(); $are_buttons = array(); $providers = msort($providers, 'getLoginOrder'); foreach ($providers as $provider) { if ($provider->isLoginFormAButton()) { $are_buttons[] = $provider->buildLoginForm($this); } else { $not_buttons[] = $provider->buildLoginForm($this); } } $out = array(); $out[] = $not_buttons; if ($are_buttons) { require_celerity_resource('auth-css'); foreach ($are_buttons as $key => $button) { $are_buttons[$key] = phutil_tag( 'div', array( 'class' => 'phabricator-login-button mmb', ), $button); } // If we only have one button, add a second pretend button so that we // always have two columns. This makes it easier to get the alignments // looking reasonable. if (count($are_buttons) == 1) { $are_buttons[] = null; } $button_columns = id(new AphrontMultiColumnView()) ->setFluidLayout(true); $are_buttons = array_chunk($are_buttons, ceil(count($are_buttons) / 2)); foreach ($are_buttons as $column) { $button_columns->addColumn($column); } $out[] = phutil_tag( 'div', array( 'class' => 'phabricator-login-buttons', ), $button_columns); } $login_message = PhabricatorEnv::getEnvConfig('auth.login-message'); $login_message = phutil_safe_html($login_message); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Login')); $crumbs->setBorder(true); return $this->buildApplicationPage( array( $crumbs, $login_message, $out, ), array( 'title' => pht('Login to Phabricator'), )); } private function processAjaxRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); // We end up here if the user clicks a workflow link that they need to // login to use. We give them a dialog saying "You need to login...". if ($request->isDialogFormPost()) { return id(new AphrontRedirectResponse())->setURI( $request->getRequestURI()); } $dialog = new AphrontDialogView(); $dialog->setUser($viewer); $dialog->setTitle(pht('Login Required')); $dialog->appendChild(pht('You must login to continue.')); $dialog->addSubmitButton(pht('Login')); $dialog->addCancelButton('/'); return id(new AphrontDialogResponse())->setDialog($dialog); } private function processConduitRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); // A common source of errors in Conduit client configuration is getting // the request path wrong. The client will end up here, so make some // effort to give them a comprehensible error message. $request_path = $this->getRequest()->getPath(); $conduit_path = '/api/'; $example_path = '/api/conduit.ping'; $message = pht( 'ERROR: You are making a Conduit API request to "%s", but the correct '. 'HTTP request path to use in order to access a COnduit method is "%s" '. '(for example, "%s"). Check your configuration.', $request_path, $conduit_path, $example_path); return id(new AphrontPlainTextResponse())->setContent($message); } protected function renderError($message) { return $this->renderErrorPage( pht('Authentication Failure'), array($message)); } } diff --git a/src/applications/auth/controller/PhabricatorLogoutController.php b/src/applications/auth/controller/PhabricatorLogoutController.php index c7543991c..cc9c63770 100644 --- a/src/applications/auth/controller/PhabricatorLogoutController.php +++ b/src/applications/auth/controller/PhabricatorLogoutController.php @@ -1,69 +1,69 @@ getRequest(); $user = $request->getUser(); if ($request->isFormPost()) { $log = PhabricatorUserLog::initializeNewLog( $user, $user->getPHID(), PhabricatorUserLog::ACTION_LOGOUT); $log->save(); // Destroy the user's session in the database so logout works even if // their cookies have some issues. We'll detect cookie issues when they // try to login again and tell them to clear any junk. $phsid = $request->getCookie(PhabricatorCookies::COOKIE_SESSION); if (strlen($phsid)) { $session = id(new PhabricatorAuthSessionQuery()) ->setViewer($user) ->withSessionKeys(array($phsid)) ->executeOne(); if ($session) { $session->delete(); } } $request->clearCookie(PhabricatorCookies::COOKIE_SESSION); return id(new AphrontRedirectResponse()) - ->setURI('/login/'); + ->setURI('/auth/loggedout/'); } if ($user->getPHID()) { $dialog = id(new AphrontDialogView()) ->setUser($user) ->setTitle(pht('Log out of Phabricator?')) ->appendChild(pht('Are you sure you want to log out?')) ->addSubmitButton(pht('Logout')) ->addCancelButton('/'); return id(new AphrontDialogResponse())->setDialog($dialog); } return id(new AphrontRedirectResponse())->setURI('/'); } } diff --git a/src/applications/auth/controller/config/PhabricatorAuthEditController.php b/src/applications/auth/controller/config/PhabricatorAuthEditController.php index 15927bb51..09742c372 100644 --- a/src/applications/auth/controller/config/PhabricatorAuthEditController.php +++ b/src/applications/auth/controller/config/PhabricatorAuthEditController.php @@ -1,335 +1,359 @@ requireApplicationCapability( AuthManageProvidersCapability::CAPABILITY); $viewer = $request->getUser(); $provider_class = $request->getURIData('className'); $config_id = $request->getURIData('id'); if ($config_id) { $config = id(new PhabricatorAuthProviderConfigQuery()) ->setViewer($viewer) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->withIDs(array($config_id)) ->executeOne(); if (!$config) { return new Aphront404Response(); } $provider = $config->getProvider(); if (!$provider) { return new Aphront404Response(); } $is_new = false; } else { $provider = null; $providers = PhabricatorAuthProvider::getAllBaseProviders(); foreach ($providers as $candidate_provider) { if (get_class($candidate_provider) === $provider_class) { $provider = $candidate_provider; break; } } if (!$provider) { return new Aphront404Response(); } // TODO: When we have multi-auth providers, support them here. $configs = id(new PhabricatorAuthProviderConfigQuery()) ->setViewer($viewer) ->withProviderClasses(array(get_class($provider))) ->execute(); if ($configs) { $id = head($configs)->getID(); $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setMethod('GET') ->setSubmitURI($this->getApplicationURI('config/edit/'.$id.'/')) ->setTitle(pht('Provider Already Configured')) ->appendChild( pht( 'This provider ("%s") already exists, and you can not add more '. 'than one instance of it. You can edit the existing provider, '. 'or you can choose a different provider.', $provider->getProviderName())) ->addCancelButton($this->getApplicationURI('config/new/')) ->addSubmitButton(pht('Edit Existing Provider')); return id(new AphrontDialogResponse())->setDialog($dialog); } $config = $provider->getDefaultProviderConfig(); $provider->attachProviderConfig($config); $is_new = true; } $errors = array(); $v_registration = $config->getShouldAllowRegistration(); $v_link = $config->getShouldAllowLink(); $v_unlink = $config->getShouldAllowUnlink(); $v_trust_email = $config->getShouldTrustEmails(); + $v_auto_login = $config->getShouldAutoLogin(); if ($request->isFormPost()) { $properties = $provider->readFormValuesFromRequest($request); list($errors, $issues, $properties) = $provider->processEditForm( $request, $properties); $xactions = array(); if (!$errors) { if ($is_new) { if (!strlen($config->getProviderType())) { $config->setProviderType($provider->getProviderType()); } if (!strlen($config->getProviderDomain())) { $config->setProviderDomain($provider->getProviderDomain()); } } $xactions[] = id(new PhabricatorAuthProviderConfigTransaction()) ->setTransactionType( PhabricatorAuthProviderConfigTransaction::TYPE_REGISTRATION) ->setNewValue($request->getInt('allowRegistration', 0)); $xactions[] = id(new PhabricatorAuthProviderConfigTransaction()) ->setTransactionType( PhabricatorAuthProviderConfigTransaction::TYPE_LINK) ->setNewValue($request->getInt('allowLink', 0)); $xactions[] = id(new PhabricatorAuthProviderConfigTransaction()) ->setTransactionType( PhabricatorAuthProviderConfigTransaction::TYPE_UNLINK) ->setNewValue($request->getInt('allowUnlink', 0)); $xactions[] = id(new PhabricatorAuthProviderConfigTransaction()) ->setTransactionType( PhabricatorAuthProviderConfigTransaction::TYPE_TRUST_EMAILS) ->setNewValue($request->getInt('trustEmails', 0)); + if ($provider instanceof PhabricatorPhabricatorAuthProvider) { + $xactions[] = id(new PhabricatorAuthProviderConfigTransaction()) + ->setTransactionType( + PhabricatorAuthProviderConfigTransaction::TYPE_AUTO_LOGIN) + ->setNewValue($request->getInt('autoLogin', 0)); + } + foreach ($properties as $key => $value) { $xactions[] = id(new PhabricatorAuthProviderConfigTransaction()) ->setTransactionType( PhabricatorAuthProviderConfigTransaction::TYPE_PROPERTY) ->setMetadataValue('auth:property', $key) ->setNewValue($value); } if ($is_new) { $config->save(); } $editor = id(new PhabricatorAuthProviderConfigEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->applyTransactions($config, $xactions); if ($provider->hasSetupStep() && $is_new) { $id = $config->getID(); $next_uri = $this->getApplicationURI('config/edit/'.$id.'/'); } else { $next_uri = $this->getApplicationURI(); } return id(new AphrontRedirectResponse())->setURI($next_uri); } } else { $properties = $provider->readFormValuesFromProvider(); $issues = array(); } if ($is_new) { if ($provider->hasSetupStep()) { $button = pht('Next Step'); } else { $button = pht('Add Provider'); } $crumb = pht('Add Provider'); $title = pht('Add Authentication Provider'); $cancel_uri = $this->getApplicationURI('/config/new/'); } else { $button = pht('Save'); $crumb = pht('Edit Provider'); $title = pht('Edit Authentication Provider'); $cancel_uri = $this->getApplicationURI(); } $config_name = 'auth.email-domains'; $config_href = '/config/edit/'.$config_name.'/'; $email_domains = PhabricatorEnv::getEnvConfig($config_name); if ($email_domains) { $registration_warning = pht( 'Users will only be able to register with a verified email address '. 'at one of the configured [[ %s | %s ]] domains: **%s**', $config_href, $config_name, implode(', ', $email_domains)); } else { $registration_warning = pht( "NOTE: Any user who can browse to this install's login page will be ". "able to register a Phabricator account. To restrict who can register ". "an account, configure [[ %s | %s ]].", $config_href, $config_name); } $str_registration = array( phutil_tag('strong', array(), pht('Allow Registration:')), ' ', pht( 'Allow users to register new Phabricator accounts using this '. 'provider. If you disable registration, users can still use this '. 'provider to log in to existing accounts, but will not be able to '. 'create new accounts.'), ); $str_link = hsprintf( '%s: %s', pht('Allow Linking Accounts'), pht( 'Allow users to link account credentials for this provider to '. 'existing Phabricator accounts. There is normally no reason to '. 'disable this unless you are trying to move away from a provider '. 'and want to stop users from creating new account links.')); $str_unlink = hsprintf( '%s: %s', pht('Allow Unlinking Accounts'), pht( 'Allow users to unlink account credentials for this provider from '. 'existing Phabricator accounts. If you disable this, Phabricator '. 'accounts will be permanently bound to provider accounts.')); $str_trusted_email = hsprintf( '%s: %s', pht('Trust Email Addresses'), pht( 'Phabricator will skip email verification for accounts registered '. 'through this provider.')); + $str_auto_login = hsprintf( + '%s: %s', + pht('Allow Auto Login'), + pht( + 'Phabricator will automatically login with this provider if it is '. + 'the only available provider.')); $status_tag = id(new PHUITagView()) ->setType(PHUITagView::TYPE_STATE); if ($is_new) { $status_tag ->setName(pht('New Provider')) ->setBackgroundColor('blue'); } else if ($config->getIsEnabled()) { $status_tag ->setName(pht('Enabled')) ->setBackgroundColor('green'); } else { $status_tag ->setName(pht('Disabled')) ->setBackgroundColor('red'); } $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Provider')) ->setValue($provider->getProviderName())) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Status')) ->setValue($status_tag)) ->appendChild( id(new AphrontFormCheckboxControl()) ->setLabel(pht('Allow')) ->addCheckbox( 'allowRegistration', 1, $str_registration, $v_registration)) ->appendRemarkupInstructions($registration_warning) ->appendChild( id(new AphrontFormCheckboxControl()) ->addCheckbox( 'allowLink', 1, $str_link, $v_link)) ->appendChild( id(new AphrontFormCheckboxControl()) ->addCheckbox( 'allowUnlink', 1, $str_unlink, $v_unlink)); if ($provider->shouldAllowEmailTrustConfiguration()) { $form->appendChild( id(new AphrontFormCheckboxControl()) ->addCheckbox( 'trustEmails', 1, $str_trusted_email, $v_trust_email)); } + if ($provider instanceof PhabricatorPhabricatorAuthProvider) { + $form->appendChild( + id(new AphrontFormCheckboxControl()) + ->addCheckbox( + 'autoLogin', + 1, + $str_auto_login, + $v_auto_login)); + } + $provider->extendEditForm($request, $form, $properties, $issues); $form ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($cancel_uri) ->setValue($button)); $help = $provider->getConfigurationHelp(); if ($help) { $form->appendChild(id(new PHUIFormDividerControl())); $form->appendRemarkupInstructions($help); } $footer = $provider->renderConfigurationFooter(); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($crumb); $timeline = null; if (!$is_new) { $timeline = $this->buildTransactionTimeline( $config, new PhabricatorAuthProviderConfigTransactionQuery()); $xactions = $timeline->getTransactions(); foreach ($xactions as $xaction) { $xaction->setProvider($provider); } } $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormErrors($errors) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $form_box, $footer, $timeline, ), array( 'title' => $title, )); } } diff --git a/src/applications/auth/editor/PhabricatorAuthProviderConfigEditor.php b/src/applications/auth/editor/PhabricatorAuthProviderConfigEditor.php index 684b6cc13..cdb021678 100644 --- a/src/applications/auth/editor/PhabricatorAuthProviderConfigEditor.php +++ b/src/applications/auth/editor/PhabricatorAuthProviderConfigEditor.php @@ -1,114 +1,121 @@ getTransactionType()) { case PhabricatorAuthProviderConfigTransaction::TYPE_ENABLE: if ($object->getIsEnabled() === null) { return null; } else { return (int)$object->getIsEnabled(); } case PhabricatorAuthProviderConfigTransaction::TYPE_REGISTRATION: return (int)$object->getShouldAllowRegistration(); case PhabricatorAuthProviderConfigTransaction::TYPE_LINK: return (int)$object->getShouldAllowLink(); case PhabricatorAuthProviderConfigTransaction::TYPE_UNLINK: return (int)$object->getShouldAllowUnlink(); case PhabricatorAuthProviderConfigTransaction::TYPE_TRUST_EMAILS: return (int)$object->getShouldTrustEmails(); + case PhabricatorAuthProviderConfigTransaction::TYPE_AUTO_LOGIN: + return (int)$object->getShouldAutoLogin(); case PhabricatorAuthProviderConfigTransaction::TYPE_PROPERTY: $key = $xaction->getMetadataValue( PhabricatorAuthProviderConfigTransaction::PROPERTY_KEY); return $object->getProperty($key); } } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorAuthProviderConfigTransaction::TYPE_ENABLE: case PhabricatorAuthProviderConfigTransaction::TYPE_REGISTRATION: case PhabricatorAuthProviderConfigTransaction::TYPE_LINK: case PhabricatorAuthProviderConfigTransaction::TYPE_UNLINK: case PhabricatorAuthProviderConfigTransaction::TYPE_TRUST_EMAILS: + case PhabricatorAuthProviderConfigTransaction::TYPE_AUTO_LOGIN: case PhabricatorAuthProviderConfigTransaction::TYPE_PROPERTY: return $xaction->getNewValue(); } } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $v = $xaction->getNewValue(); switch ($xaction->getTransactionType()) { case PhabricatorAuthProviderConfigTransaction::TYPE_ENABLE: return $object->setIsEnabled($v); case PhabricatorAuthProviderConfigTransaction::TYPE_REGISTRATION: return $object->setShouldAllowRegistration($v); case PhabricatorAuthProviderConfigTransaction::TYPE_LINK: return $object->setShouldAllowLink($v); case PhabricatorAuthProviderConfigTransaction::TYPE_UNLINK: return $object->setShouldAllowUnlink($v); case PhabricatorAuthProviderConfigTransaction::TYPE_TRUST_EMAILS: return $object->setShouldTrustEmails($v); + case PhabricatorAuthProviderConfigTransaction::TYPE_AUTO_LOGIN: + return $object->setShouldAutoLogin($v); case PhabricatorAuthProviderConfigTransaction::TYPE_PROPERTY: $key = $xaction->getMetadataValue( PhabricatorAuthProviderConfigTransaction::PROPERTY_KEY); return $object->setProperty($key, $v); } } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { return; } protected function mergeTransactions( PhabricatorApplicationTransaction $u, PhabricatorApplicationTransaction $v) { $type = $u->getTransactionType(); switch ($type) { case PhabricatorAuthProviderConfigTransaction::TYPE_ENABLE: case PhabricatorAuthProviderConfigTransaction::TYPE_REGISTRATION: case PhabricatorAuthProviderConfigTransaction::TYPE_LINK: case PhabricatorAuthProviderConfigTransaction::TYPE_UNLINK: case PhabricatorAuthProviderConfigTransaction::TYPE_TRUST_EMAILS: + case PhabricatorAuthProviderConfigTransaction::TYPE_AUTO_LOGIN: // For these types, last transaction wins. return $v; } return parent::mergeTransactions($u, $v); } } diff --git a/src/applications/auth/provider/PhabricatorAuthProvider.php b/src/applications/auth/provider/PhabricatorAuthProvider.php index 6c34babac..0745fd2af 100644 --- a/src/applications/auth/provider/PhabricatorAuthProvider.php +++ b/src/applications/auth/provider/PhabricatorAuthProvider.php @@ -1,486 +1,486 @@ providerConfig = $config; return $this; } public function hasProviderConfig() { return (bool)$this->providerConfig; } public function getProviderConfig() { if ($this->providerConfig === null) { throw new Exception( 'Call attachProviderConfig() before getProviderConfig()!'); } return $this->providerConfig; } public function getConfigurationHelp() { return null; } public function getDefaultProviderConfig() { return id(new PhabricatorAuthProviderConfig()) ->setProviderClass(get_class($this)) ->setIsEnabled(1) ->setShouldAllowLogin(1) ->setShouldAllowRegistration(1) ->setShouldAllowLink(1) ->setShouldAllowUnlink(1); } public function getNameForCreate() { return $this->getProviderName(); } public function getDescriptionForCreate() { return null; } public function getProviderKey() { return $this->getAdapter()->getAdapterKey(); } public function getProviderType() { return $this->getAdapter()->getAdapterType(); } public function getProviderDomain() { return $this->getAdapter()->getAdapterDomain(); } public static function getAllBaseProviders() { static $providers; if ($providers === null) { $objects = id(new PhutilSymbolLoader()) ->setAncestorClass(__CLASS__) ->loadObjects(); $providers = $objects; } return $providers; } public static function getAllProviders() { static $providers; if ($providers === null) { $objects = self::getAllBaseProviders(); $configs = id(new PhabricatorAuthProviderConfigQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->execute(); $providers = array(); foreach ($configs as $config) { if (!isset($objects[$config->getProviderClass()])) { // This configuration is for a provider which is not installed. continue; } $object = clone $objects[$config->getProviderClass()]; $object->attachProviderConfig($config); $key = $object->getProviderKey(); if (isset($providers[$key])) { throw new Exception( pht( "Two authentication providers use the same provider key ". "('%s'). Each provider must be identified by a unique key.", $key)); } $providers[$key] = $object; } } return $providers; } public static function getAllEnabledProviders() { $providers = self::getAllProviders(); foreach ($providers as $key => $provider) { if (!$provider->isEnabled()) { unset($providers[$key]); } } return $providers; } public static function getEnabledProviderByKey($provider_key) { return idx(self::getAllEnabledProviders(), $provider_key); } abstract public function getProviderName(); abstract public function getAdapter(); public function isEnabled() { return $this->getProviderConfig()->getIsEnabled(); } public function shouldAllowLogin() { return $this->getProviderConfig()->getShouldAllowLogin(); } public function shouldAllowRegistration() { return $this->getProviderConfig()->getShouldAllowRegistration(); } public function shouldAllowAccountLink() { return $this->getProviderConfig()->getShouldAllowLink(); } public function shouldAllowAccountUnlink() { return $this->getProviderConfig()->getShouldAllowUnlink(); } public function shouldTrustEmails() { return $this->shouldAllowEmailTrustConfiguration() && $this->getProviderConfig()->getShouldTrustEmails(); } /** * Should we allow the adapter to be marked as "trusted". This is true for * all adapters except those that allow the user to type in emails (see * @{class:PhabricatorPasswordAuthProvider}). */ public function shouldAllowEmailTrustConfiguration() { return true; } public function buildLoginForm(PhabricatorAuthStartController $controller) { return $this->renderLoginForm($controller->getRequest(), $mode = 'start'); } abstract public function processLoginRequest( PhabricatorAuthLoginController $controller); public function buildLinkForm(PhabricatorAuthLinkController $controller) { return $this->renderLoginForm($controller->getRequest(), $mode = 'link'); } public function shouldAllowAccountRefresh() { return true; } public function buildRefreshForm( PhabricatorAuthLinkController $controller) { return $this->renderLoginForm($controller->getRequest(), $mode = 'refresh'); } protected function renderLoginForm(AphrontRequest $request, $mode) { throw new PhutilMethodNotImplementedException(); } public function createProviders() { return array($this); } protected function willSaveAccount(PhabricatorExternalAccount $account) { return; } public function willRegisterAccount(PhabricatorExternalAccount $account) { return; } protected function loadOrCreateAccount($account_id) { if (!strlen($account_id)) { throw new Exception('loadOrCreateAccount(...): empty account ID!'); } $adapter = $this->getAdapter(); $adapter_class = get_class($adapter); if (!strlen($adapter->getAdapterType())) { throw new Exception( "AuthAdapter (of class '{$adapter_class}') has an invalid ". "implementation: no adapter type."); } if (!strlen($adapter->getAdapterDomain())) { throw new Exception( "AuthAdapter (of class '{$adapter_class}') has an invalid ". "implementation: no adapter domain."); } $account = id(new PhabricatorExternalAccount())->loadOneWhere( 'accountType = %s AND accountDomain = %s AND accountID = %s', $adapter->getAdapterType(), $adapter->getAdapterDomain(), $account_id); if (!$account) { $account = id(new PhabricatorExternalAccount()) ->setAccountType($adapter->getAdapterType()) ->setAccountDomain($adapter->getAdapterDomain()) ->setAccountID($account_id); } $account->setUsername($adapter->getAccountName()); $account->setRealName($adapter->getAccountRealName()); $account->setEmail($adapter->getAccountEmail()); $account->setAccountURI($adapter->getAccountURI()); $account->setProfileImagePHID(null); $image_uri = $adapter->getAccountImageURI(); if ($image_uri) { try { $name = PhabricatorSlug::normalize($this->getProviderName()); $name = $name.'-profile.jpg'; // TODO: If the image has not changed, we do not need to make a new // file entry for it, but there's no convenient way to do this with // PhabricatorFile right now. The storage will get shared, so the impact // here is negligible. $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $image_file = PhabricatorFile::newFromFileDownload( $image_uri, array( 'name' => $name, 'canCDN' => true, )); unset($unguarded); if ($image_file) { $account->setProfileImagePHID($image_file->getPHID()); } } catch (Exception $ex) { // Log this but proceed, it's not especially important that we // be able to pull profile images. phlog($ex); } } $this->willSaveAccount($account); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $account->save(); unset($unguarded); return $account; } public function getLoginURI() { $app = PhabricatorApplication::getByClass('PhabricatorAuthApplication'); return $app->getApplicationURI('/login/'.$this->getProviderKey().'/'); } public function getSettingsURI() { return '/settings/panel/external/'; } public function getStartURI() { $app = PhabricatorApplication::getByClass('PhabricatorAuthApplication'); $uri = $app->getApplicationURI('/start/'); return $uri; } public function isDefaultRegistrationProvider() { return false; } public function shouldRequireRegistrationPassword() { return false; } public function getDefaultExternalAccount() { throw new PhutilMethodNotImplementedException(); } public function getLoginOrder() { return '500-'.$this->getProviderName(); } protected function getLoginIcon() { return 'Generic'; } public function isLoginFormAButton() { return false; } public function renderConfigPropertyTransactionTitle( PhabricatorAuthProviderConfigTransaction $xaction) { return null; } public function readFormValuesFromProvider() { return array(); } public function readFormValuesFromRequest(AphrontRequest $request) { return array(); } public function processEditForm( AphrontRequest $request, array $values) { $errors = array(); $issues = array(); return array($errors, $issues, $values); } public function extendEditForm( AphrontRequest $request, AphrontFormView $form, array $values, array $issues) { return; } public function willRenderLinkedAccount( PhabricatorUser $viewer, PHUIObjectItemView $item, PhabricatorExternalAccount $account) { $account_view = id(new PhabricatorAuthAccountView()) ->setExternalAccount($account) ->setAuthProvider($this); $item->appendChild( phutil_tag( 'div', array( 'class' => 'mmr mml mst mmb', ), $account_view)); } /** * Return true to use a two-step configuration (setup, configure) instead of * the default single-step configuration. In practice, this means that * creating a new provider instance will redirect back to the edit page * instead of the provider list. * * @return bool True if this provider uses two-step configuration. */ public function hasSetupStep() { return false; } /** * Render a standard login/register button element. * * The `$attributes` parameter takes these keys: * * - `uri`: URI the button should take the user to when clicked. * - `method`: Optional HTTP method the button should use, defaults to GET. * * @param AphrontRequest HTTP request. * @param string Request mode string. * @param map Additional parameters, see above. * @return wild Login button. */ protected function renderStandardLoginButton( AphrontRequest $request, $mode, array $attributes = array()) { PhutilTypeSpec::checkMap( $attributes, array( 'method' => 'optional string', 'uri' => 'string', 'sigil' => 'optional string', )); $viewer = $request->getUser(); $adapter = $this->getAdapter(); if ($mode == 'link') { $button_text = pht('Link External Account'); } else if ($mode == 'refresh') { $button_text = pht('Refresh Account Link'); } else if ($this->shouldAllowRegistration()) { $button_text = pht('Login or Register'); } else { $button_text = pht('Login'); } $icon = id(new PHUIIconView()) ->setSpriteSheet(PHUIIconView::SPRITE_LOGIN) ->setSpriteIcon($this->getLoginIcon()); $button = id(new PHUIButtonView()) ->setSize(PHUIButtonView::BIG) ->setColor(PHUIButtonView::GREY) ->setIcon($icon) ->setText($button_text) ->setSubtext($this->getProviderName()); $uri = $attributes['uri']; $uri = new PhutilURI($uri); $params = $uri->getQueryParams(); $uri->setQueryParams(array()); $content = array($button); foreach ($params as $key => $value) { $content[] = phutil_tag( 'input', array( 'type' => 'hidden', 'name' => $key, 'value' => $value, )); } return phabricator_form( $viewer, array( 'method' => idx($attributes, 'method', 'GET'), 'action' => (string)$uri, 'sigil' => idx($attributes, 'sigil'), ), $content); } public function renderConfigurationFooter() { return null; } - protected function getAuthCSRFCode(AphrontRequest $request) { + public function getAuthCSRFCode(AphrontRequest $request) { $phcid = $request->getCookie(PhabricatorCookies::COOKIE_CLIENTID); if (!strlen($phcid)) { throw new Exception( pht( 'Your browser did not submit a "%s" cookie with client state '. 'information in the request. Check that cookies are enabled. '. 'If this problem persists, you may need to clear your cookies.', PhabricatorCookies::COOKIE_CLIENTID)); } return PhabricatorHash::digest($phcid); } protected function verifyAuthCSRFCode(AphrontRequest $request, $actual) { $expect = $this->getAuthCSRFCode($request); if (!strlen($actual)) { throw new Exception( pht( 'The authentication provider did not return a client state '. 'parameter in its response, but one was expected. If this '. 'problem persists, you may need to clear your cookies.')); } if ($actual !== $expect) { throw new Exception( pht( 'The authentication provider did not return the correct client '. 'state parameter in its response. If this problem persists, you may '. 'need to clear your cookies.')); } } } diff --git a/src/applications/auth/storage/PhabricatorAuthProviderConfig.php b/src/applications/auth/storage/PhabricatorAuthProviderConfig.php index cced9a0cc..14d68b352 100644 --- a/src/applications/auth/storage/PhabricatorAuthProviderConfig.php +++ b/src/applications/auth/storage/PhabricatorAuthProviderConfig.php @@ -1,135 +1,137 @@ true, self::CONFIG_SERIALIZATION => array( 'properties' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'isEnabled' => 'bool', 'providerClass' => 'text128', 'providerType' => 'text32', 'providerDomain' => 'text128', 'shouldAllowLogin' => 'bool', 'shouldAllowRegistration' => 'bool', 'shouldAllowLink' => 'bool', 'shouldAllowUnlink' => 'bool', 'shouldTrustEmails' => 'bool', + 'shouldAutoLogin' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_provider' => array( 'columns' => array('providerType', 'providerDomain'), 'unique' => true, ), 'key_class' => array( 'columns' => array('providerClass'), ), ), ) + parent::getConfiguration(); } public function getProperty($key, $default = null) { return idx($this->properties, $key, $default); } public function setProperty($key, $value) { $this->properties[$key] = $value; return $this; } public function getProvider() { if (!$this->provider) { $base = PhabricatorAuthProvider::getAllBaseProviders(); $found = null; foreach ($base as $provider) { if (get_class($provider) == $this->providerClass) { $found = $provider; break; } } if ($found) { $this->provider = id(clone $found)->attachProviderConfig($this); } } return $this->provider; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorAuthProviderConfigEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorAuthProviderConfigTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( 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_USER; case PhabricatorPolicyCapability::CAN_EDIT: return PhabricatorPolicies::POLICY_ADMIN; } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } } diff --git a/src/applications/auth/storage/PhabricatorAuthProviderConfigTransaction.php b/src/applications/auth/storage/PhabricatorAuthProviderConfigTransaction.php index 1dfdf033b..a0be28f3a 100644 --- a/src/applications/auth/storage/PhabricatorAuthProviderConfigTransaction.php +++ b/src/applications/auth/storage/PhabricatorAuthProviderConfigTransaction.php @@ -1,154 +1,166 @@ provider = $provider; return $this; } public function getProvider() { return $this->provider; } public function getApplicationName() { return 'auth'; } public function getApplicationTransactionType() { return PhabricatorAuthAuthProviderPHIDType::TYPECONST; } public function getApplicationTransactionCommentObject() { return null; } public function getIcon() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_ENABLE: if ($new) { return 'fa-play'; } else { return 'fa-pause'; } } return parent::getIcon(); } public function getColor() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_ENABLE: if ($new) { return 'green'; } else { return 'red'; } } return parent::getColor(); } public function getTitle() { $author_phid = $this->getAuthorPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_ENABLE: if ($old === null) { return pht( '%s created this provider.', $this->renderHandleLink($author_phid)); } else if ($new) { return pht( '%s enabled this provider.', $this->renderHandleLink($author_phid)); } else { return pht( '%s disabled this provider.', $this->renderHandleLink($author_phid)); } break; case self::TYPE_REGISTRATION: if ($new) { return pht( '%s enabled registration.', $this->renderHandleLink($author_phid)); } else { return pht( '%s disabled registration.', $this->renderHandleLink($author_phid)); } break; case self::TYPE_LINK: if ($new) { return pht( '%s enabled accont linking.', $this->renderHandleLink($author_phid)); } else { return pht( '%s disabled account linking.', $this->renderHandleLink($author_phid)); } break; case self::TYPE_UNLINK: if ($new) { return pht( '%s enabled account unlinking.', $this->renderHandleLink($author_phid)); } else { return pht( '%s disabled account unlinking.', $this->renderHandleLink($author_phid)); } break; case self::TYPE_TRUST_EMAILS: if ($new) { return pht( '%s enabled email trust.', $this->renderHandleLink($author_phid)); } else { return pht( '%s disabled email trust.', $this->renderHandleLink($author_phid)); } break; + case self::TYPE_AUTO_LOGIN: + if ($new) { + return pht( + '%s enabled auto login.', + $this->renderHandleLink($author_phid)); + } else { + return pht( + '%s disabled auto login.', + $this->renderHandleLink($author_phid)); + } + break; case self::TYPE_PROPERTY: $provider = $this->getProvider(); if ($provider) { $title = $provider->renderConfigPropertyTransactionTitle($this); if (strlen($title)) { return $title; } } return pht( '%s edited a property of this provider.', $this->renderHandleLink($author_phid)); break; } return parent::getTitle(); } }