IndexController.php #1

  • //
  • guest/
  • perforce_software/
  • chronicle/
  • main/
  • application/
  • user/
  • controllers/
  • IndexController.php
  • View
  • Commits
  • Open Download .zip Download (27 KB)
<?php
/**
 * Manages user operations (e.g. login/logout).
 *
 * @copyright   2011 Perforce Software. All rights reserved.
 * @license     Please see LICENSE.txt in top-level folder of this distribution.
 * @version     <release>/<patch>
 */
class User_IndexController extends Zend_Controller_Action
{
    const   LOGIN_FAILED_MIN_DELAY  = 1000000;
    const   LOGIN_FAILED_MAX_DELAY  = 2000000;

    public $contexts = array(
        'login'     => array('partial', 'json'),
        'add'       => array('partial', 'json'),
        'edit'      => array('partial', 'json'),
        'index'     => array('json'),
        'delete'    => array('json'),
    );

    /**
     * Use management layout for all actions.
     */
    public function init()
    {
        $this->_helper->layout->setLayout('manage-layout');
    }

    /**
     * List all users.
     *
     * @publishes   p4cms.user.grid.actions
     *              Modify the passed menu (add/modify/delete items) to influence the actions shown
     *              on entries in the Manage Users grid.
     *              P4Cms_Navigation            $actions    A menu to hold grid actions.
     *
     * @publishes   p4cms.user.grid.data.item
     *              Return the passed item after applying any modifications (add properties, change
     *              values, etc.) to influence the row values sent to the Manage Users grid.
     *              array                       $item       The item to potentially modify.
     *              mixed                       $model      The original object/array that was used
     *                                                      to make the item.
     *              Ui_View_Helper_DataGrid     $helper     The view helper that broadcast this
     *                                                      topic.
     *
     * @publishes   p4cms.user.grid.data
     *              Adjust the passed data (add properties, modify values, etc.) to influence the
     *              row values sent to the Manage Users grid.
     *              Zend_Dojo_Data              $data       The data to be filtered.
     *              Ui_View_Helper_DataGrid     $helper     The view helper that broadcast this
     *                                                      topic.
     *
     * @publishes   p4cms.user.grid.populate
     *              Adjust the passed iterator (possibly based on values in the passed form) to
     *              filter which users will be shown on the Manage Users grid.
     *              P4Cms_Model_Iterator    $users          An Iterator of User_Model_User objects.
     *              P4Cms_Form_PubSubForm   $form           A form containing filter options.
     *
     * @publishes   p4cms.user.grid.render
     *              Make adjustments to the datagrid helper's options pre-render (e.g. change
     *              options to add columns) for the Manage Users grid.
     *              Ui_View_Helper_DataGrid     $helper     The view helper that broadcast this
     *                                                      topic.
     *
     * @publishes   p4cms.user.grid.form
     *              Make arbitrary modifications to the Manage Users filters form.
     *              P4Cms_Form_PubSubForm       $form       The form that published this event.
     *
     * @publishes   p4cms.user.grid.form.subForms
     *              Return a Form (or array of Forms) to have them added to the Manage Users filters
     *              form. The returned form(s) should have a 'name' set on them to allow them to be
     *              uniquely identified.
     *              P4Cms_Form_PubSubForm       $form       The form that published this event.
     *
     * @publishes   p4cms.user.grid.form.preValidate
     *              Allows subscribers to adjust the Manage Users filters form prior to
     *              validation of the passed data. For example, modify element values based on
     *              related selections to permit proper validation.
     *              P4Cms_Form_PubSubForm       $form       The form that published this event.
     *              array                       $values     An associative array of form values.
     *
     * @publishes   p4cms.user.grid.form.validate
     *              Return false to indicate the Manage Users filters form is invalid. Return true
     *              to indicate your custom checks were satisfied, so form validity should be
     *              unchanged.
     *              P4Cms_Form_PubSubForm       $form       The form that published this event.
     *              array                       $values     An associative array of form values.
     *
     * @publishes   p4cms.user.grid.form.populate
     *              Allows subscribers to adjust the Manage Users filters form after it has been
     *              populated with the passed data.
     *              P4Cms_Form_PubSubForm       $form       The form that published this event.
     *              array                       $values     The values passed to the populate
     *                                                      method.
     */
    public function indexAction()
    {
        // enforce permissions
        $this->acl->check('users', 'manage');

        // setup list options form.
        $request        = $this->getRequest();
        $gridNamespace  = 'p4cms.user.grid';
        $form           = new Ui_Form_GridOptions(
            array(
                'namespace'   => $gridNamespace
            )
        );
        $form->populate($request->getParams());

        // setup view.
        $view               = $this->view;
        $view->form         = $form;
        $view->pageSize     = $request->getParam('count', 100);
        $view->rowOffset    = $request->getParam('start', 0);
        $view->pageOffset   = round($view->rowOffset / $view->pageSize, 0) + 1;
        $view->showAddLink  = $this->acl->isAllowed('users', 'add');
        $view->headTitle()->set('Manage Users');

        // set DataGrid view helper namespace
        $helper = $view->dataGrid();
        $helper->setNamespace($gridNamespace);

        // collect the actions from interested parties
        $actions = new P4Cms_Navigation;
        P4Cms_PubSub::publish($gridNamespace . '.actions', $actions);
        $view->actions = $actions;

        // early exit for standard requests (ie. not json)
        if (!$this->contextSwitch->getCurrentContext()) {
            $this->getHelper('helpUrl')->setUrl('users.management.html');
            return;
        }

        // fetch users - allow third-parties to influence list
        $users = P4Cms_User::fetchAll();
        try {
            P4Cms_PubSub::publish($gridNamespace . '.populate', $users, $form);
        } catch (Exception $e) {
            P4Cms_Log::logException("Error building user list.", $e);
        }

        // prepare sorting options
        $sortKey    = $request->getParam('sort', 'id');
        $sortFlags  = array(
            P4Cms_Model_Iterator::SORT_NATURAL,
            P4Cms_Model_Iterator::SORT_NO_CASE
        );
        if (substr($sortKey, 0, 1) == '-') {
            $sortKey = substr($sortKey, 1);
            $sortFlags[] = P4Cms_Model_Iterator::SORT_DESCENDING;
        } else {
            $sortFlags[] = P4Cms_Model_Iterator::SORT_ASCENDING;
        }

        // apply sorting options.
        $users->sortBy($sortKey, $sortFlags);

        // add users to the view.
        $view->users = $users;
    }

    /**
     * Handle user logins.
     * If not posted, presents form; otherwise, authenticates.
     */
    public function loginAction()
    {
        $request            = $this->getRequest();
        $form               = $this->_getLoginForm();

        // set up view
        $view               = $this->view;
        $view->form         = $form;
        $view->headTitle()->set('User Login');

        // if posted, validate form and authenticate user.
        if ($request->isPost()) {
            // if form is invalid, set response code and exit
            if (!$form->isValid($request->getPost())) {
                $this->getResponse()->setHttpResponseCode(400);
                return;
            }

            $login = $form->getValue('user');

            // silently clear login if it is does not look like
            // a valid p4 username or email address.
            $userValidator  = new P4_Validate_UserName;
            $emailValidator = new Zend_Validate_EmailAddress;
            if (!$userValidator->isValid($login) && !$emailValidator->isValid($login)) {
                $login = null;
            }

            // if login is an email address, lookup corresponding usernames.
            if (strpos($login, '@')) {
                $found = array(); // list with users matching the email address
                foreach (P4Cms_User::fetchAll() as $user) {
                    if ($user->getEmail() !== $login) {
                        continue;
                    }

                    $found[] = $user->getId();
                }

                $login = $found ?: null;
            }

            if (!is_array($login)) {
                $login = array($login);
            }

            // loop through all login candidates
            $errorMessage = 'Login failed. Invalid user or password.';
            foreach ($login as $loginCandidate) {

                // try to authenticate with given password
                $result = $this->_authenticate($loginCandidate, $form->getValue('password'));
                if (!$result->isValid()) {
                    // don't allow successful auth without priveleges, and report correct error
                    if ($result->getCode() === Zend_Auth_Result::FAILURE_IDENTITY_AMBIGUOUS) {
                        $errorMessage = 'You do not have permission to access this site.';
                        break;
                    }
                    continue;
                }

                // don't allow login as system user
                if ($this->_isSystemUser($loginCandidate)) {
                    Zend_Auth::getInstance()->clearIdentity();
                    $errorMessage = 'Login failed. Cannot login as the system user.';
                    break;
                }

                // auth data in session shouldn't influence page caching; add ourselves to the ignore list
                if (P4Cms_Cache::canCache('page')) {
                    P4Cms_Cache::getCache('page')->addIgnoredSessionVariable(
                        Zend_Auth::getInstance()->getStorage()->getNamespace()
                    );
                }

                // protect against session fixation
                Zend_Session::regenerateId();

                // login successful
                P4Cms_Notifications::add(
                    'Login successful.',
                    P4Cms_Notifications::SEVERITY_SUCCESS
                );

                // redirect for traditional contexts
                if (!$this->contextSwitch->getCurrentContext()) {
                    $this->redirector->gotoUrl($request->getBaseUrl());
                }

                return;
            }

            // login failed, add random 1-2 second delay.
            $delay = mt_rand(self::LOGIN_FAILED_MIN_DELAY, self::LOGIN_FAILED_MAX_DELAY);
            usleep($delay);

            // authentication failed - add error to form.
            if (!$this->contextSwitch->getCurrentContext()) {
                P4Cms_Notifications::add($errorMessage, P4Cms_Notifications::SEVERITY_ERROR);
            }

            $form->addError($errorMessage);
            $this->getResponse()->setHttpResponseCode(400);
        }
    }

    /**
     * Log the user out (clear identity).
     */
    public function logoutAction()
    {
        Zend_Auth::getInstance()->clearIdentity();
        P4Cms_Notifications::add('Logout completed.', P4Cms_Notifications::SEVERITY_SUCCESS);
        $this->redirector->gotoUrl($this->getRequest()->getBaseUrl());
    }

    /**
     * Add a new user.
     */
    public function addAction()
    {
        // enforce permissions
        $this->acl->check('users', 'add');

        $request    = $this->getRequest();
        $activeUser = P4Cms_User::fetchActive();
        $form       = new User_Form_Add;

        // deny if adding user is disabled
        if (!$activeUser->isAdministrator() && !P4_User::isAutoUserCreationEnabled()) {
            throw new P4Cms_AccessDeniedException(
                "You don't have permission to add users."
            );
        }

        // if we are connected to a P4 server using external authentication,
        // disable the password setting because it cannot be done without
        // an old password.  adding a description to the top of the dialog.
        $externalAuth = $activeUser->getAdapter()->getConnection()->hasExternalAuth();
        if ($externalAuth) {
            $note = "Your Perforce Server is using external authentication. An entry for this <br/>"
                  . "user must be added to the external authentication system before the <br/>"
                  . "user can log into Chronicle.";
            $form->removeElement('password');
            $form->removeElement('passwordConfirm');

            // add a note field
            $form->addElement(
                'note',
                'note',
                array(
                    'value'     => $note
                )
            );
            $form->getElement('note')
                 ->removeDecorator('label')
                 ->getDecorator('htmlTag')
                 ->setOption('class', 'user-note');
        }

        // set up view
        $view               = $this->view;
        $view->form         = $form;
        $view->headTitle()->set('Add User');

        // prepare default roles
        $defaultRoles = P4Cms_Acl_Role::exists(P4Cms_Acl_Role::ROLE_MEMBER)
            ? array(P4Cms_Acl_Role::ROLE_MEMBER)
            : array();

        // set default roles if no post
        if (!$request->isPost()) {
            $request->setParam('roles', $defaultRoles);
        }

        // populate form from request
        $form->populate($request->getParams());

        // if posted, validate form and save user.
        if ($request->isPost()) {
            // if form is invalid, set response code and exit
            if (!$form->isValid($request->getParams())) {
                $this->getResponse()->setHttpResponseCode(400);
                $view->errors = $form->getMessages();
                return;
            }

            // create the user entry.
            $user = new P4Cms_User;
            $user->setValues($form->getValues())
                 ->save();

            // set roles
            //  - if active user is not administrator, use site storage adapter
            //  - if user has permission, take roles from request, otherwise use defaults
            $adapter = $activeUser->isAdministrator()
                ? $activeUser->getPersonalAdapter()
                : P4Cms_Site::fetchActive()->getStorageAdapter();
            $roles   = $form->getElement('roles') && $this->acl->isAllowed('users', 'manage-roles')
                ? $form->getValue('roles')
                : $defaultRoles;
            P4Cms_Acl_Role::setUserRoles($user, $roles, $adapter);

            // if active user is anonymous, log in as newly created user
            if (P4Cms_User::fetchActive()->isAnonymous()) {
                $result = $this->_authenticate($user->getId(), $form->getValue('password'));
                if ($result->isValid()) {
                    P4Cms_Notifications::add(
                        "You have been logged in as '{$user->getId()}'",
                        P4Cms_Notifications::SEVERITY_SUCCESS
                    );
                }
            }

            // set notification message
            $view->message = "User '{$user->getId()}' has been successfuly added.";

            // for traditional requests, add notification message and redirect
            if (!$this->contextSwitch->getCurrentContext()) {
                P4Cms_Notifications::add(
                    $view->message,
                    P4Cms_Notifications::SEVERITY_SUCCESS
                );
                $this->redirector->gotoUrl($request->getBaseUrl());
            }
        }
    }

    /**
     * Edit an existing user entry.
     */
    public function editAction()
    {
        $request    = $this->getRequest();
        $activeUser = P4Cms_User::fetchActive();
        $userId     = $request->getParam('id', $activeUser->getId());

        // ensure user id is set in the request
        $request->setParam('id', $userId);

        // enforce permissions
        if ($userId !== $activeUser->getId()) {
            $this->acl->check('users', 'manage');
        }

        // deny if user id is null or attempted to edit system user
        if ($userId === null || $this->_isSystemUser($userId)) {
            throw new P4Cms_AccessDeniedException(
                "You don't have permission to edit this user."
            );
        }

        // determine if we can change password.
        // when connected to a P4 server using external authentication,
        // passwords cannot be changed if an auth-set trigger has not
        // been configured or trying to change other user's password.
        $connection        = $activeUser->getAdapter()->getConnection();
        $canChangePassword = !$connection->hasExternalAuth()
                             || ($connection->hasAuthSetTrigger() && ($activeUser->getId() === $userId));

        // determine whether old password input is neccessary when setting up new password
        $formOptions = array(
            'needOldPassword'   => !$activeUser->isAdministrator()
                || ($activeUser->getId() === $userId),
            'canChangePassword' => $canChangePassword
        );

        // determine whether the user is the last administrator
        $admins = P4Cms_Acl_Role::fetch(P4Cms_Acl_Role::ROLE_ADMINISTRATOR)->getRealUsers();
        $formOptions['requireAdministrator'] = count($admins) == 1
            && in_array($userId, $admins);

        // redirect to page not found if user doesn't exist
        if (!P4Cms_User::exists($userId)) {
            return $this->_forward('page-not-found', 'index', 'error');
        }

        $user = P4Cms_User::fetch($userId);

        // set up view
        $form               = new User_Form_Edit($formOptions);
        $view               = $this->view;
        $view->form         = $form;
        $view->user         = $user;
        $view->headTitle()->set('Edit User');

        // set roles from storage if no post, or not permitted.
        if (!$request->isPost() || !$this->acl->isAllowed('users', 'manage-roles')) {
            $request->setParam('roles', $user->getRoles()->invoke('getId'));
        }

        // populate form from request if posted, otherwise from storage.
        $form->populate(
            $request->isPost()
            ? $request->getParams()
            : $request->getParams() + $user->getValues()
        );

        // if change password unchecked, disable 'password' group.
        if (!$form->getValue('changePassword')) {
            $group = $form->getDisplayGroup('passwords');
            $group->setAttrib('class', $group->getAttrib('class') . ' disabled');
        }

        // if posted, validate form and save user.
        if ($request->isPost()) {
            // if form is invalid, set response code and exit
            if (!$form->isValid($request->getParams())) {
                $this->getResponse()->setHttpResponseCode(400);
                $view->errors = $form->getMessages();
                return;
            }

            $user->setValues($form->getValues());

            // if current password given, must set password explicitly.
            if ($form->getValue('currentPassword')) {
                $user->setPassword(
                    $form->getValue('password'),
                    $form->getValue('currentPassword')
                );
            }

            // we now try to save the user
            // if we are using external auth, there are several cases to handle:
            //  - auth set trigger fails (extract message and set errror on form)
            //  - subsequent login fails - auth-set didn't work correctly (tell user to use ext. auth)
            $externalAuth = $connection->hasExternalAuth();
            try {
                $user->save();
            } catch (P4_Exception $e) {
                $error   = false;
                $message = $e->getMessage();
                if ($externalAuth && stristr($message, "Command failed: Password not changed.")) {
                    $error = preg_replace('/^.*validation failed:/s', '', $message);
                    if (trim($error) === "no error message") {
                        $error = true;
                    }
                } else if ($externalAuth && ($e instanceof P4_Connection_LoginException)) {
                    $error = true;
                }

                // if this is an expected case, report as validation error.
                if ($error) {
                    if (!is_string($error)) {
                        $error = "Your Perforce Server is using external authentication. "
                               . "Please change the user's password in the external authentication system.";
                    }
                    $form->getElement('password')->addError(trim($error));
                    $this->getResponse()->setHttpResponseCode(400);
                    $view->errors = $form->getMessages();
                    return;
                }

                // unexpected/unhandled case.
                throw $e;
            }

            // if user has permission to manage roles, set roles from request
            if ($form->getElement('roles') && $this->acl->isAllowed('users', 'manage-roles')) {
                P4Cms_Acl_Role::setUserRoles($user, $form->getValue('roles'));
            }

            // if current user changed the password, re-authenticate to get updated ticket
            if ($activeUser->getId() === $userId && $form->getValue('changePassword')) {
                $result = $this->_authenticate($userId, $form->getValue('password'));
            }

            // set notification message
            $view->message = "User '{$user->getId()}' has been successfuly updated.";

            // clear any cache entries related to this user
            P4Cms_Cache::clean('all', 'p4cms_user_' . md5($user->getId()));

            // for traditional requests, add notification message and redirect
            if (!$this->contextSwitch->getCurrentContext()) {
                P4Cms_Notifications::add(
                    $view->message,
                    P4Cms_Notifications::SEVERITY_SUCCESS
                );
                $this->redirector->gotoUrl($request->getBaseUrl());
            }
        }
    }

    /**
     *  Delete user entry.
     *	User account can be removed only via post.
     */
    public function deleteAction()
    {
        // deny if not accessed via post
        $request = $this->getRequest();
        if (!$request->isPost()) {
            throw new P4Cms_AccessDeniedException(
                "Deleting users is not permitted in this context."
            );
        }

        $activeUser       = P4Cms_User::fetchActive();
        $userId           = $request->getPost('id');
        $deleteActiveUser = $userId === $activeUser->getId();

        if (!$deleteActiveUser) {
            $this->acl->check('users', 'manage');
        }

        // deny if attempting to delete system user
        if ($this->_isSystemUser($userId)) {
            throw new P4Cms_AccessDeniedException(
                "You don't have permission to delete this user."
            );
        }

        // get user to delete
        $user = $deleteActiveUser
            ? $activeUser
            : P4Cms_User::fetch($userId);

        // deleted user should have the same adapter as active user personal adapter
        $user->setAdapter($activeUser->getPersonalAdapter());

        // deny if deleted user is the only administrator
        if ($user->isAdministrator()
             && count(P4Cms_Acl_Role::fetch(P4Cms_Acl_Role::ROLE_ADMINISTRATOR)->getUsers()) == 1
        ) {
            throw new P4Cms_AccessDeniedException(
                "The only administrator cannot be deleted."
            );
        }

        // remove user references from the roles that user is associated with
        // use personal adapter if user is admin, otherwise use site adapter.
        $adapter = $activeUser->isAdministrator()
                 ? $activeUser->getPersonalAdapter()
                 : P4Cms_Site::fetchActive()->getStorageAdapter();
        P4Cms_Acl_Role::setUserRoles($user, array(), $adapter);

        // do the actual delete
        $user->delete();

        // clear any cache entries related to this user
        P4Cms_Cache::clean('all', 'p4cms_user_' . md5($user->getId()));

        // add notification if active user was deleted or
        // if we are in traditional context
        $context = $this->contextSwitch->getCurrentContext();
        if (!$context || $deleteActiveUser) {
            P4Cms_Notifications::add(
                "User '$userId' has been deleted.",
                P4Cms_Notifications::SEVERITY_SUCCESS
            );
        }

        // redirect for traditional requests
        if (!$context) {
            if ($deleteActiveUser) {
                $this->redirector->gotoUrl($request->getBaseUrl());
            } else {
                $this->redirector->gotoSimple('index');
            }
        }

        $this->view->userId           = $userId;
        $this->view->deleteActiveUser = $deleteActiveUser;
    }

    /**
     * Get the login form.
     *
     * @return  User_Form_Login     the login form.
     */
    protected function _getLoginForm()
    {
        // grab current acl.
        $acl = $this->acl->getAcl();

        $form = new User_Form_Login(
            array(
                'action' => $this->view->url(
                    array(
                        'module'        => 'user',
                        'controller'    => 'index',
                        'action'        => 'login'
                    )
                ),
                'acl'   => $acl
            )
        );

        // for context switched requests, prefix form ids with context
        // to ensure ids are unique if they appear twice on the page.
        $context = $this->contextSwitch->getCurrentContext();
        if ($context) {
            $form->setIdPrefix($context . "-");
        }

        return $form;
    }

    /**
     * Authenticate the user. Return true if success, otherwise false.
     *
     * @param   string  $login      user's id
     * @param   string  $password   user's password
     * @return  Zend_Auth_Result    The result of the authentication attempt.
     */
    protected function _authenticate($login, $password)
    {
        // construct user instance to authenticate against.
        $user = new P4Cms_User;
        $user->setId($login)
             ->setPassword($password);

        // authenticate
        return Zend_Auth::getInstance()->authenticate($user);
    }

    /**
     * Check if the given user is the system user.
     *
     * @param   string      $userId     the user id to compare against the system user id.
     * @return  boolean     true if the given user is the system user; otherwise false.
     */
    protected function _isSystemUser($userId)
    {
        return $userId == $this->getInvokeArg('bootstrap')->getResource('perforce')->getUser();
    }
}
# Change User Description Committed
#1 16170 perforce_software Move Chronicle files to follow new path scheme for branching.
//guest/perforce_software/chronicle/application/user/controllers/IndexController.php
#1 8972 Matt Attaway Initial add of the Chronicle source code