1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-14 02:42:40 +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:
epriestley 2012-05-21 12:47:38 -07:00
parent 3fd93b594e
commit 77f546c572
16 changed files with 265 additions and 19 deletions

View file

@ -453,6 +453,10 @@ return array(
// disabled. // disabled.
'auth.sshkeys.enabled' => false, '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 -------------------------------------------------------------- // // -- Accounts -------------------------------------------------------------- //

View file

@ -726,6 +726,7 @@ phutil_register_library_map(array(
'PhabricatorMetaMTASendGridReceiveController' => 'applications/metamta/controller/sendgridreceive', 'PhabricatorMetaMTASendGridReceiveController' => 'applications/metamta/controller/sendgridreceive',
'PhabricatorMetaMTAViewController' => 'applications/metamta/controller/view', 'PhabricatorMetaMTAViewController' => 'applications/metamta/controller/view',
'PhabricatorMetaMTAWorker' => 'applications/metamta/worker', 'PhabricatorMetaMTAWorker' => 'applications/metamta/worker',
'PhabricatorMustVerifyEmailController' => 'applications/auth/controller/mustverify',
'PhabricatorMySQLFileStorageEngine' => 'applications/files/engine/mysql', 'PhabricatorMySQLFileStorageEngine' => 'applications/files/engine/mysql',
'PhabricatorNotificationsController' => 'applications/notifications/controller/base', 'PhabricatorNotificationsController' => 'applications/notifications/controller/base',
'PhabricatorOAuthClientAuthorization' => 'applications/oauthserver/storage/clientauthorization', 'PhabricatorOAuthClientAuthorization' => 'applications/oauthserver/storage/clientauthorization',
@ -1647,6 +1648,7 @@ phutil_register_library_map(array(
'PhabricatorMetaMTASendGridReceiveController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTASendGridReceiveController' => 'PhabricatorMetaMTAController',
'PhabricatorMetaMTAViewController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTAViewController' => 'PhabricatorMetaMTAController',
'PhabricatorMetaMTAWorker' => 'PhabricatorWorker', 'PhabricatorMetaMTAWorker' => 'PhabricatorWorker',
'PhabricatorMustVerifyEmailController' => 'PhabricatorAuthController',
'PhabricatorMySQLFileStorageEngine' => 'PhabricatorFileStorageEngine', 'PhabricatorMySQLFileStorageEngine' => 'PhabricatorFileStorageEngine',
'PhabricatorNotificationsController' => 'PhabricatorController', 'PhabricatorNotificationsController' => 'PhabricatorController',
'PhabricatorOAuthClientAuthorization' => 'PhabricatorOAuthServerDAO', 'PhabricatorOAuthClientAuthorization' => 'PhabricatorOAuthServerDAO',

View file

@ -133,6 +133,7 @@ class AphrontDefaultApplicationConfiguration
'etoken/(?P<token>\w+)/' => 'PhabricatorEmailTokenController', 'etoken/(?P<token>\w+)/' => 'PhabricatorEmailTokenController',
'refresh/' => 'PhabricatorRefreshCSRFController', 'refresh/' => 'PhabricatorRefreshCSRFController',
'validate/' => 'PhabricatorLoginValidateController', 'validate/' => 'PhabricatorLoginValidateController',
'mustverify/' => 'PhabricatorMustVerifyEmailController',
), ),
'/logout/' => 'PhabricatorLogoutController', '/logout/' => 'PhabricatorLogoutController',

View file

@ -23,6 +23,11 @@ final class PhabricatorLogoutController
return true; return true;
} }
public function shouldRequireEmailVerification() {
// Allow unverified users to logout.
return false;
}
public function shouldRequireEnabledUser() { public function shouldRequireEnabledUser() {
// Allow disabled users to logout. // Allow disabled users to logout.
return false; return false;

View file

@ -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',
));
}
}

View 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');

View file

@ -30,6 +30,15 @@ abstract class PhabricatorController extends AphrontController {
return true; 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() { final public function willBeginExecution() {
$request = $this->getRequest(); $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()) { if ($this->shouldRequireLogin() && !$user->getPHID()) {
$login_controller = newv('PhabricatorLoginController', array($request)); $login_controller = newv('PhabricatorLoginController', array($request));
return $this->delegateToController($login_controller); return $this->delegateToController($login_controller);

View file

@ -261,8 +261,9 @@ final class PhabricatorConduitAPIController
if ($request->getUser()->getPHID()) { if ($request->getUser()->getPHID()) {
$request->validateCSRF(); $request->validateCSRF();
$api_request->setUser($request->getUser()); return $this->validateAuthenticatedUser(
return null; $api_request,
$request->getUser());
} }
// handle oauth // handle oauth
@ -303,8 +304,9 @@ final class PhabricatorConduitAPIController
'Access token is for invalid user.', 'Access token is for invalid user.',
); );
} }
$api_request->setUser($user); return $this->validateAuthenticatedUser(
return null; $api_request,
$user);
} }
// Handle sessionless auth. TOOD: This is super messy. // Handle sessionless auth. TOOD: This is super messy.
@ -327,8 +329,9 @@ final class PhabricatorConduitAPIController
'Authentication is invalid.', 'Authentication is invalid.',
); );
} }
$api_request->setUser($user); return $this->validateAuthenticatedUser(
return null; $api_request,
$user);
} }
$session_key = idx($metadata, 'sessionKey'); $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; return null;
} }

View file

@ -18,6 +18,7 @@ phutil_require_module('phabricator', 'applications/oauthserver/server');
phutil_require_module('phabricator', 'applications/oauthserver/storage/accesstoken'); phutil_require_module('phabricator', 'applications/oauthserver/storage/accesstoken');
phutil_require_module('phabricator', 'applications/people/storage/user'); phutil_require_module('phabricator', 'applications/people/storage/user');
phutil_require_module('phabricator', 'infrastructure/accesslog'); phutil_require_module('phabricator', 'infrastructure/accesslog');
phutil_require_module('phabricator', 'infrastructure/env');
phutil_require_module('phabricator', 'storage/queryfx'); phutil_require_module('phabricator', 'storage/queryfx');
phutil_require_module('phabricator', 'view/control/table'); phutil_require_module('phabricator', 'view/control/table');
phutil_require_module('phabricator', 'view/layout/panel'); phutil_require_module('phabricator', 'view/layout/panel');

View file

@ -254,6 +254,14 @@ final class PhabricatorPeopleEditController
->setDisabled($is_immutable) ->setDisabled($is_immutable)
->setValue($new_email) ->setValue($new_email)
->setError($e_email)); ->setError($e_email));
} else {
$form->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Email')
->setValue(
$user->loadPrimaryEmail()->getIsVerified()
? 'Verified'
: 'Unverified'));
} }
$form->appendChild($this->getRoleInstructions()); $form->appendChild($this->getRoleInstructions());

View file

@ -25,6 +25,12 @@ final class PhabricatorEmailVerificationController
$this->code = $data['code']; $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() { public function processRequest() {
$request = $this->getRequest(); $request = $this->getRequest();
$user = $request->getUser(); $user = $request->getUser();
@ -34,6 +40,14 @@ final class PhabricatorEmailVerificationController
$user->getPHID(), $user->getPHID(),
$this->code); $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( $settings_link = phutil_render_tag(
'a', 'a',
array( array(
@ -42,6 +56,7 @@ final class PhabricatorEmailVerificationController
'Return to Email Settings'); 'Return to Email Settings');
$settings_link = '<br /><p><strong>'.$settings_link.'</strong></p>'; $settings_link = '<br /><p><strong>'.$settings_link.'</strong></p>';
if (!$email) { if (!$email) {
$content = id(new AphrontErrorView()) $content = id(new AphrontErrorView())
->setTitle('Unable To Verify') ->setTitle('Unable To Verify')
@ -68,6 +83,7 @@ final class PhabricatorEmailVerificationController
->setTitle('Address Verified') ->setTitle('Address Verified')
->appendChild( ->appendChild(
'<p>This email address has now been verified. Thanks!</p>'. '<p>This email address has now been verified. Thanks!</p>'.
$home_link.
$settings_link); $settings_link);
} }

View file

@ -37,23 +37,30 @@ final class PhabricatorPeopleListController
$pager->setCount($count); $pager->setCount($count);
$pager->setURI($request->getRequestURI(), 'page'); $pager->setURI($request->getRequestURI(), 'page');
$users = id(new PhabricatorUser())->loadAllWhere( $users = id(new PhabricatorPeopleQuery())
'1 = 1 ORDER BY id DESC LIMIT %d, %d', ->needPrimaryEmail(true)
$pager->getOffset(), ->executeWithPager($pager);
$pager->getPageSize());
$rows = array(); $rows = array();
foreach ($users as $user) { foreach ($users as $user) {
if ($user->getPrimaryEmail()->getIsVerified()) {
$status = ''; $email = 'Verified';
if ($user->getIsDisabled()) {
$status = 'Disabled';
} else if ($user->getIsAdmin()) {
$status = 'Admin';
} else { } 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( $rows[] = array(
phabricator_date($user->getDateCreated(), $viewer), phabricator_date($user->getDateCreated(), $viewer),
phabricator_time($user->getDateCreated(), $viewer), phabricator_time($user->getDateCreated(), $viewer),
@ -65,6 +72,7 @@ final class PhabricatorPeopleListController
phutil_escape_html($user->getUserName())), phutil_escape_html($user->getUserName())),
phutil_escape_html($user->getRealName()), phutil_escape_html($user->getRealName()),
$status, $status,
$email,
phutil_render_tag( phutil_render_tag(
'a', 'a',
array( array(
@ -82,7 +90,8 @@ final class PhabricatorPeopleListController
'Time', 'Time',
'Username', 'Username',
'Real Name', 'Real Name',
'Status', 'Roles',
'Email',
'', '',
)); ));
$table->setColumnClasses( $table->setColumnClasses(
@ -92,6 +101,7 @@ final class PhabricatorPeopleListController
'pri', 'pri',
'wide', 'wide',
null, null,
null,
'action', 'action',
)); ));
$table->setColumnVisibility( $table->setColumnVisibility(
@ -102,6 +112,7 @@ final class PhabricatorPeopleListController
true, true,
$is_admin, $is_admin,
$is_admin, $is_admin,
$is_admin,
)); ));
$panel = new AphrontPanelView(); $panel = new AphrontPanelView();

View file

@ -7,6 +7,7 @@
phutil_require_module('phabricator', 'applications/people/controller/base'); 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', 'applications/people/storage/user');
phutil_require_module('phabricator', 'storage/queryfx'); phutil_require_module('phabricator', 'storage/queryfx');
phutil_require_module('phabricator', 'view/control/pager'); phutil_require_module('phabricator', 'view/control/pager');

View file

@ -23,6 +23,8 @@ final class PhabricatorPeopleQuery extends PhabricatorOffsetPagedQuery {
private $phids; private $phids;
private $ids; private $ids;
private $needPrimaryEmail;
public function withIds(array $ids) { public function withIds(array $ids) {
$this->ids = $ids; $this->ids = $ids;
return $this; return $this;
@ -44,6 +46,11 @@ final class PhabricatorPeopleQuery extends PhabricatorOffsetPagedQuery {
return $this; return $this;
} }
public function needPrimaryEmail($need) {
$this->needPrimaryEmail = $need;
return $this;
}
public function execute() { public function execute() {
$table = new PhabricatorUser(); $table = new PhabricatorUser();
$conn_r = $table->establishConnection('r'); $conn_r = $table->establishConnection('r');
@ -62,6 +69,16 @@ final class PhabricatorPeopleQuery extends PhabricatorOffsetPagedQuery {
$users = $table->loadAllFromArray($data); $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; return $users;
} }

View file

@ -12,5 +12,7 @@ phutil_require_module('phabricator', 'infrastructure/query/offsetpaged');
phutil_require_module('phabricator', 'storage/qsprintf'); phutil_require_module('phabricator', 'storage/qsprintf');
phutil_require_module('phabricator', 'storage/queryfx'); phutil_require_module('phabricator', 'storage/queryfx');
phutil_require_module('phutil', 'utils');
phutil_require_source('PhabricatorPeopleQuery.php'); phutil_require_source('PhabricatorPeopleQuery.php');

View file

@ -41,6 +41,7 @@ final class PhabricatorUser extends PhabricatorUserDAO {
protected $isDisabled = 0; protected $isDisabled = 0;
private $preferences = null; private $preferences = null;
private $primaryEmail;
protected function readField($field) { protected function readField($field) {
switch ($field) { switch ($field) {
@ -418,6 +419,19 @@ final class PhabricatorUser extends PhabricatorUserDAO {
1); 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() { public function loadPreferences() {
if ($this->preferences) { if ($this->preferences) {
return $this->preferences; return $this->preferences;