mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-09 16:32:39 +01:00
Allow installs to require email verification
Summary: Allow installs to require users to verify email addresses before they can use Phabricator. If a user logs in without a verified email address, they're given instructions to verify their address. This isn't too useful on its own since we don't actually have arbitrary email registration, but the next step is to allow installs to restrict email to only some domains (e.g., @mycompany.com). Test Plan: - Verification - Set verification requirement to `true`. - Tried to use Phabricator with an unverified account, was told to verify. - Tried to use Conduit, was given a verification error. - Verified account, used Phabricator. - Unverified account, reset password, verified implicit verification, used Phabricator. - People Admin Interface - Viewed as admin. Clicked "Administrate User". - Viewed as non-admin - Sanity Checks - Used Conduit normally from web/CLI with a verified account. - Logged in/out. - Sent password reset email. - Created a new user. - Logged in with an unverified user but with the configuration set to off. Reviewers: btrahan, vrana, jungejason Reviewed By: btrahan CC: aran, csilvers Maniphest Tasks: T1184 Differential Revision: https://secure.phabricator.com/D2520
This commit is contained in:
parent
3fd93b594e
commit
77f546c572
16 changed files with 265 additions and 19 deletions
|
@ -453,6 +453,10 @@ return array(
|
|||
// disabled.
|
||||
'auth.sshkeys.enabled' => false,
|
||||
|
||||
// If true, email addresses must be verified (by clicking a link in an
|
||||
// email) before a user can login. By default, verification is optional.
|
||||
'auth.require-email-verification' => false,
|
||||
|
||||
|
||||
// -- Accounts -------------------------------------------------------------- //
|
||||
|
||||
|
|
|
@ -726,6 +726,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorMetaMTASendGridReceiveController' => 'applications/metamta/controller/sendgridreceive',
|
||||
'PhabricatorMetaMTAViewController' => 'applications/metamta/controller/view',
|
||||
'PhabricatorMetaMTAWorker' => 'applications/metamta/worker',
|
||||
'PhabricatorMustVerifyEmailController' => 'applications/auth/controller/mustverify',
|
||||
'PhabricatorMySQLFileStorageEngine' => 'applications/files/engine/mysql',
|
||||
'PhabricatorNotificationsController' => 'applications/notifications/controller/base',
|
||||
'PhabricatorOAuthClientAuthorization' => 'applications/oauthserver/storage/clientauthorization',
|
||||
|
@ -1647,6 +1648,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorMetaMTASendGridReceiveController' => 'PhabricatorMetaMTAController',
|
||||
'PhabricatorMetaMTAViewController' => 'PhabricatorMetaMTAController',
|
||||
'PhabricatorMetaMTAWorker' => 'PhabricatorWorker',
|
||||
'PhabricatorMustVerifyEmailController' => 'PhabricatorAuthController',
|
||||
'PhabricatorMySQLFileStorageEngine' => 'PhabricatorFileStorageEngine',
|
||||
'PhabricatorNotificationsController' => 'PhabricatorController',
|
||||
'PhabricatorOAuthClientAuthorization' => 'PhabricatorOAuthServerDAO',
|
||||
|
|
|
@ -133,6 +133,7 @@ class AphrontDefaultApplicationConfiguration
|
|||
'etoken/(?P<token>\w+)/' => 'PhabricatorEmailTokenController',
|
||||
'refresh/' => 'PhabricatorRefreshCSRFController',
|
||||
'validate/' => 'PhabricatorLoginValidateController',
|
||||
'mustverify/' => 'PhabricatorMustVerifyEmailController',
|
||||
),
|
||||
|
||||
'/logout/' => 'PhabricatorLogoutController',
|
||||
|
|
|
@ -23,6 +23,11 @@ final class PhabricatorLogoutController
|
|||
return true;
|
||||
}
|
||||
|
||||
public function shouldRequireEmailVerification() {
|
||||
// Allow unverified users to logout.
|
||||
return false;
|
||||
}
|
||||
|
||||
public function shouldRequireEnabledUser() {
|
||||
// Allow disabled users to logout.
|
||||
return false;
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Copyright 2012 Facebook, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
final class PhabricatorMustVerifyEmailController
|
||||
extends PhabricatorAuthController {
|
||||
|
||||
public function shouldRequireLogin() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function shouldRequireEmailVerification() {
|
||||
// NOTE: We don't technically need this since PhabricatorController forces
|
||||
// us here in either case, but it's more consistent with intent.
|
||||
return false;
|
||||
}
|
||||
|
||||
public function processRequest() {
|
||||
$request = $this->getRequest();
|
||||
$user = $request->getUser();
|
||||
|
||||
$email = $user->loadPrimaryEmail();
|
||||
|
||||
if ($email->getIsVerified()) {
|
||||
return id(new AphrontRedirectResponse())->setURI('/');
|
||||
}
|
||||
|
||||
$email_address = $email->getAddress();
|
||||
|
||||
$sent = null;
|
||||
if ($request->isFormPost()) {
|
||||
$email->sendVerificationEmail($user);
|
||||
$sent = new AphrontErrorView();
|
||||
$sent->setSeverity(AphrontErrorView::SEVERITY_NOTICE);
|
||||
$sent->setTitle('Email Sent');
|
||||
$sent->appendChild(
|
||||
'<p>Another verification email was sent to <strong>'.
|
||||
phutil_escape_html($email_address).'</strong>.</p>');
|
||||
}
|
||||
|
||||
$error_view = new AphrontRequestFailureView();
|
||||
$error_view->setHeader('Check Your Email');
|
||||
$error_view->appendChild(
|
||||
'<p>You must verify your email address to login. You should have a new '.
|
||||
'email message from Phabricator with verification instructions in your '.
|
||||
'inbox (<strong>'.phutil_escape_html($email_address).'</strong>).</p>');
|
||||
$error_view->appendChild(
|
||||
'<p>If you did not receive an email, you can click the button below '.
|
||||
'to try sending another one.</p>');
|
||||
$error_view->appendChild(
|
||||
'<div class="aphront-failure-continue">'.
|
||||
phabricator_render_form(
|
||||
$user,
|
||||
array(
|
||||
'action' => '/login/mustverify/',
|
||||
'method' => 'POST',
|
||||
),
|
||||
phutil_render_tag(
|
||||
'button',
|
||||
array(
|
||||
),
|
||||
'Send Another Email')).
|
||||
'</div>');
|
||||
|
||||
|
||||
return $this->buildStandardPageResponse(
|
||||
array(
|
||||
$sent,
|
||||
$error_view,
|
||||
),
|
||||
array(
|
||||
'title' => 'Must Verify Email',
|
||||
));
|
||||
}
|
||||
|
||||
}
|
19
src/applications/auth/controller/mustverify/__init__.php
Normal file
19
src/applications/auth/controller/mustverify/__init__.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
/**
|
||||
* This file is automatically generated. Lint this module to rebuild it.
|
||||
* @generated
|
||||
*/
|
||||
|
||||
|
||||
|
||||
phutil_require_module('phabricator', 'aphront/response/redirect');
|
||||
phutil_require_module('phabricator', 'applications/auth/controller/base');
|
||||
phutil_require_module('phabricator', 'infrastructure/javelin/markup');
|
||||
phutil_require_module('phabricator', 'view/form/error');
|
||||
phutil_require_module('phabricator', 'view/page/failure');
|
||||
|
||||
phutil_require_module('phutil', 'markup');
|
||||
phutil_require_module('phutil', 'utils');
|
||||
|
||||
|
||||
phutil_require_source('PhabricatorMustVerifyEmailController.php');
|
|
@ -30,6 +30,15 @@ abstract class PhabricatorController extends AphrontController {
|
|||
return true;
|
||||
}
|
||||
|
||||
public function shouldRequireEmailVerification() {
|
||||
$config_key = 'auth.require-email-verification';
|
||||
|
||||
$need_verify = PhabricatorEnv::getEnvConfig($config_key);
|
||||
$need_login = $this->shouldRequireLogin();
|
||||
|
||||
return ($need_login && $need_verify);
|
||||
}
|
||||
|
||||
final public function willBeginExecution() {
|
||||
|
||||
$request = $this->getRequest();
|
||||
|
@ -70,6 +79,20 @@ abstract class PhabricatorController extends AphrontController {
|
|||
}
|
||||
}
|
||||
|
||||
if ($this->shouldRequireEmailVerification()) {
|
||||
$email = $user->loadPrimaryEmail();
|
||||
if (!$email) {
|
||||
throw new Exception(
|
||||
"No primary email address associated with this account!");
|
||||
}
|
||||
if (!$email->getIsVerified()) {
|
||||
$verify_controller = newv(
|
||||
'PhabricatorMustVerifyEmailController',
|
||||
array($request));
|
||||
return $this->delegateToController($verify_controller);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->shouldRequireLogin() && !$user->getPHID()) {
|
||||
$login_controller = newv('PhabricatorLoginController', array($request));
|
||||
return $this->delegateToController($login_controller);
|
||||
|
|
|
@ -261,8 +261,9 @@ final class PhabricatorConduitAPIController
|
|||
|
||||
if ($request->getUser()->getPHID()) {
|
||||
$request->validateCSRF();
|
||||
$api_request->setUser($request->getUser());
|
||||
return null;
|
||||
return $this->validateAuthenticatedUser(
|
||||
$api_request,
|
||||
$request->getUser());
|
||||
}
|
||||
|
||||
// handle oauth
|
||||
|
@ -303,8 +304,9 @@ final class PhabricatorConduitAPIController
|
|||
'Access token is for invalid user.',
|
||||
);
|
||||
}
|
||||
$api_request->setUser($user);
|
||||
return null;
|
||||
return $this->validateAuthenticatedUser(
|
||||
$api_request,
|
||||
$user);
|
||||
}
|
||||
|
||||
// Handle sessionless auth. TOOD: This is super messy.
|
||||
|
@ -327,8 +329,9 @@ final class PhabricatorConduitAPIController
|
|||
'Authentication is invalid.',
|
||||
);
|
||||
}
|
||||
$api_request->setUser($user);
|
||||
return null;
|
||||
return $this->validateAuthenticatedUser(
|
||||
$api_request,
|
||||
$user);
|
||||
}
|
||||
|
||||
$session_key = idx($metadata, 'sessionKey');
|
||||
|
@ -364,7 +367,36 @@ final class PhabricatorConduitAPIController
|
|||
);
|
||||
}
|
||||
|
||||
$api_request->setUser($user);
|
||||
return $this->validateAuthenticatedUser(
|
||||
$api_request,
|
||||
$user);
|
||||
}
|
||||
|
||||
private function validateAuthenticatedUser(
|
||||
ConduitAPIRequest $request,
|
||||
PhabricatorUser $user) {
|
||||
|
||||
if ($user->getIsDisabled()) {
|
||||
return array(
|
||||
'ERR-USER-DISABLED',
|
||||
'User is disabled.');
|
||||
}
|
||||
|
||||
if (PhabricatorEnv::getEnvConfig('auth.require-email-verification')) {
|
||||
$email = $user->loadPrimaryEmail();
|
||||
if (!$email) {
|
||||
return array(
|
||||
'ERR-USER-NOEMAIL',
|
||||
'User has no primary email address.');
|
||||
}
|
||||
if (!$email->getIsVerified()) {
|
||||
return array(
|
||||
'ERR-USER-UNVERIFIED',
|
||||
'User has unverified email address.');
|
||||
}
|
||||
}
|
||||
|
||||
$request->setUser($user);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ phutil_require_module('phabricator', 'applications/oauthserver/server');
|
|||
phutil_require_module('phabricator', 'applications/oauthserver/storage/accesstoken');
|
||||
phutil_require_module('phabricator', 'applications/people/storage/user');
|
||||
phutil_require_module('phabricator', 'infrastructure/accesslog');
|
||||
phutil_require_module('phabricator', 'infrastructure/env');
|
||||
phutil_require_module('phabricator', 'storage/queryfx');
|
||||
phutil_require_module('phabricator', 'view/control/table');
|
||||
phutil_require_module('phabricator', 'view/layout/panel');
|
||||
|
|
|
@ -254,6 +254,14 @@ final class PhabricatorPeopleEditController
|
|||
->setDisabled($is_immutable)
|
||||
->setValue($new_email)
|
||||
->setError($e_email));
|
||||
} else {
|
||||
$form->appendChild(
|
||||
id(new AphrontFormStaticControl())
|
||||
->setLabel('Email')
|
||||
->setValue(
|
||||
$user->loadPrimaryEmail()->getIsVerified()
|
||||
? 'Verified'
|
||||
: 'Unverified'));
|
||||
}
|
||||
|
||||
$form->appendChild($this->getRoleInstructions());
|
||||
|
|
|
@ -25,6 +25,12 @@ final class PhabricatorEmailVerificationController
|
|||
$this->code = $data['code'];
|
||||
}
|
||||
|
||||
public function shouldRequireEmailVerification() {
|
||||
// Since users need to be able to hit this endpoint in order to verify
|
||||
// email, we can't ever require email verification here.
|
||||
return false;
|
||||
}
|
||||
|
||||
public function processRequest() {
|
||||
$request = $this->getRequest();
|
||||
$user = $request->getUser();
|
||||
|
@ -34,6 +40,14 @@ final class PhabricatorEmailVerificationController
|
|||
$user->getPHID(),
|
||||
$this->code);
|
||||
|
||||
$home_link = phutil_render_tag(
|
||||
'a',
|
||||
array(
|
||||
'href' => '/',
|
||||
),
|
||||
'Continue to Phabricator');
|
||||
$home_link = '<br /><p><strong>'.$home_link.'</strong></p>';
|
||||
|
||||
$settings_link = phutil_render_tag(
|
||||
'a',
|
||||
array(
|
||||
|
@ -42,6 +56,7 @@ final class PhabricatorEmailVerificationController
|
|||
'Return to Email Settings');
|
||||
$settings_link = '<br /><p><strong>'.$settings_link.'</strong></p>';
|
||||
|
||||
|
||||
if (!$email) {
|
||||
$content = id(new AphrontErrorView())
|
||||
->setTitle('Unable To Verify')
|
||||
|
@ -68,6 +83,7 @@ final class PhabricatorEmailVerificationController
|
|||
->setTitle('Address Verified')
|
||||
->appendChild(
|
||||
'<p>This email address has now been verified. Thanks!</p>'.
|
||||
$home_link.
|
||||
$settings_link);
|
||||
}
|
||||
|
||||
|
|
|
@ -37,23 +37,30 @@ final class PhabricatorPeopleListController
|
|||
$pager->setCount($count);
|
||||
$pager->setURI($request->getRequestURI(), 'page');
|
||||
|
||||
$users = id(new PhabricatorUser())->loadAllWhere(
|
||||
'1 = 1 ORDER BY id DESC LIMIT %d, %d',
|
||||
$pager->getOffset(),
|
||||
$pager->getPageSize());
|
||||
$users = id(new PhabricatorPeopleQuery())
|
||||
->needPrimaryEmail(true)
|
||||
->executeWithPager($pager);
|
||||
|
||||
$rows = array();
|
||||
foreach ($users as $user) {
|
||||
|
||||
$status = '';
|
||||
if ($user->getIsDisabled()) {
|
||||
$status = 'Disabled';
|
||||
} else if ($user->getIsAdmin()) {
|
||||
$status = 'Admin';
|
||||
if ($user->getPrimaryEmail()->getIsVerified()) {
|
||||
$email = 'Verified';
|
||||
} else {
|
||||
$status = '-';
|
||||
$email = 'Unverified';
|
||||
}
|
||||
|
||||
$status = array();
|
||||
if ($user->getIsDisabled()) {
|
||||
$status[] = 'Disabled';
|
||||
}
|
||||
if ($user->getIsAdmin()) {
|
||||
$status[] = 'Admin';
|
||||
}
|
||||
if ($user->getIsSystemAgent()) {
|
||||
$status[] = 'System Agent';
|
||||
}
|
||||
$status = implode(', ', $status);
|
||||
|
||||
$rows[] = array(
|
||||
phabricator_date($user->getDateCreated(), $viewer),
|
||||
phabricator_time($user->getDateCreated(), $viewer),
|
||||
|
@ -65,6 +72,7 @@ final class PhabricatorPeopleListController
|
|||
phutil_escape_html($user->getUserName())),
|
||||
phutil_escape_html($user->getRealName()),
|
||||
$status,
|
||||
$email,
|
||||
phutil_render_tag(
|
||||
'a',
|
||||
array(
|
||||
|
@ -82,7 +90,8 @@ final class PhabricatorPeopleListController
|
|||
'Time',
|
||||
'Username',
|
||||
'Real Name',
|
||||
'Status',
|
||||
'Roles',
|
||||
'Email',
|
||||
'',
|
||||
));
|
||||
$table->setColumnClasses(
|
||||
|
@ -92,6 +101,7 @@ final class PhabricatorPeopleListController
|
|||
'pri',
|
||||
'wide',
|
||||
null,
|
||||
null,
|
||||
'action',
|
||||
));
|
||||
$table->setColumnVisibility(
|
||||
|
@ -102,6 +112,7 @@ final class PhabricatorPeopleListController
|
|||
true,
|
||||
$is_admin,
|
||||
$is_admin,
|
||||
$is_admin,
|
||||
));
|
||||
|
||||
$panel = new AphrontPanelView();
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
|
||||
phutil_require_module('phabricator', 'applications/people/controller/base');
|
||||
phutil_require_module('phabricator', 'applications/people/query');
|
||||
phutil_require_module('phabricator', 'applications/people/storage/user');
|
||||
phutil_require_module('phabricator', 'storage/queryfx');
|
||||
phutil_require_module('phabricator', 'view/control/pager');
|
||||
|
|
|
@ -23,6 +23,8 @@ final class PhabricatorPeopleQuery extends PhabricatorOffsetPagedQuery {
|
|||
private $phids;
|
||||
private $ids;
|
||||
|
||||
private $needPrimaryEmail;
|
||||
|
||||
public function withIds(array $ids) {
|
||||
$this->ids = $ids;
|
||||
return $this;
|
||||
|
@ -44,6 +46,11 @@ final class PhabricatorPeopleQuery extends PhabricatorOffsetPagedQuery {
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function needPrimaryEmail($need) {
|
||||
$this->needPrimaryEmail = $need;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function execute() {
|
||||
$table = new PhabricatorUser();
|
||||
$conn_r = $table->establishConnection('r');
|
||||
|
@ -62,6 +69,16 @@ final class PhabricatorPeopleQuery extends PhabricatorOffsetPagedQuery {
|
|||
|
||||
$users = $table->loadAllFromArray($data);
|
||||
|
||||
if ($users && $this->needPrimaryEmail) {
|
||||
$emails = id(new PhabricatorUserEmail())->loadAllWhere(
|
||||
'userPHID IN (%Ls)',
|
||||
mpull($users, 'getPHID'));
|
||||
$emails = mpull($emails, null, 'getUserPHID');
|
||||
foreach ($users as $user) {
|
||||
$user->attachPrimaryEmail($emails[$user->getPHID()]);
|
||||
}
|
||||
}
|
||||
|
||||
return $users;
|
||||
}
|
||||
|
||||
|
|
|
@ -12,5 +12,7 @@ phutil_require_module('phabricator', 'infrastructure/query/offsetpaged');
|
|||
phutil_require_module('phabricator', 'storage/qsprintf');
|
||||
phutil_require_module('phabricator', 'storage/queryfx');
|
||||
|
||||
phutil_require_module('phutil', 'utils');
|
||||
|
||||
|
||||
phutil_require_source('PhabricatorPeopleQuery.php');
|
||||
|
|
|
@ -41,6 +41,7 @@ final class PhabricatorUser extends PhabricatorUserDAO {
|
|||
protected $isDisabled = 0;
|
||||
|
||||
private $preferences = null;
|
||||
private $primaryEmail;
|
||||
|
||||
protected function readField($field) {
|
||||
switch ($field) {
|
||||
|
@ -418,6 +419,19 @@ final class PhabricatorUser extends PhabricatorUserDAO {
|
|||
1);
|
||||
}
|
||||
|
||||
public function attachPrimaryEmail(PhabricatorUserEmail $email) {
|
||||
$this->primaryEmail = $email;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPrimaryEmail() {
|
||||
if ($this->primaryEmail === null) {
|
||||
throw new Exception(
|
||||
"Call attachPrimaryEmail() before getPrimaryEmail()!");
|
||||
}
|
||||
return $this->primaryEmail;
|
||||
}
|
||||
|
||||
public function loadPreferences() {
|
||||
if ($this->preferences) {
|
||||
return $this->preferences;
|
||||
|
|
Loading…
Reference in a new issue