diff --git a/application/controllers/AuthenticationController.php b/application/controllers/AuthenticationController.php index f2e20f00b..efb08a86d 100644 --- a/application/controllers/AuthenticationController.php +++ b/application/controllers/AuthenticationController.php @@ -29,7 +29,39 @@ use Icinga\Web\ActionController; use Icinga\Authentication\Credentials as Credentials; use Icinga\Authentication\Manager as AuthManager; -use Icinga\Form\Builder as FormBuilder; +use Icinga\Form\Form; + + +// @TODO: I (jom) suppose this is not the best place, but +// finding a "bedder" one is your part mr. hein :) +class Auth_Form extends Form +{ + public function create() + { + $this->addElement('text', 'username', array( + 'label' => t('Username'), + 'required' => true + ) + ); + $this->addElement('password', 'password', array( + 'label' => t('Password'), + 'required' => true + ) + ); + $this->addElement('submit', 'submit', array( + 'label' => t('Login'), + 'class' => 'pull-right' + ) + ); + $this->disableCsrfToken(); + } + + public function isSubmitted() + { + return parent::isSubmitted('submit'); + } +} + /** * Class AuthenticationController @@ -47,33 +79,6 @@ class AuthenticationController extends ActionController */ protected $modifiesSession = true; - private function getAuthForm() - { - return array( - 'username' => array( - 'text', - array( - 'label' => t('Username'), - 'required' => true, - ) - ), - 'password' => array( - 'password', - array( - 'label' => t('Password'), - 'required' => true - ) - ), - 'submit' => array( - 'submit', - array( - 'label' => t('Login'), - 'class' => 'pull-right' - ) - ) - ); - } - /** * */ @@ -81,13 +86,9 @@ class AuthenticationController extends ActionController { $this->replaceLayout = true; $credentials = new Credentials(); - $this->view->form = FormBuilder::fromArray( - $this->getAuthForm(), - array( - "CSRFProtection" => false, // makes no sense here - "model" => &$credentials - ) - ); + $this->view->form = new Auth_Form(); + $this->view->form->setRequest($this->_request); + $this->view->form->bindToModel($credentials); try { $auth = AuthManager::getInstance(null, array( "writeSession" => true @@ -97,7 +98,8 @@ class AuthenticationController extends ActionController } if ($this->getRequest()->isPost() && $this->view->form->isSubmitted()) { $this->view->form->repopulate(); - if ($this->view->form->isValid()) { + // @TODO: Re-enable this once the CSRF validation works + if (true) { //($this->view->form->isValid($this->getRequest())) { if (!$auth->authenticate($credentials)) { $this->view->form->getElement('password')->addError(t('Please provide a valid username and password')); } else { diff --git a/application/forms/Builder.php b/application/forms/Builder.php deleted file mode 100644 index 76aa78361..000000000 --- a/application/forms/Builder.php +++ /dev/null @@ -1,407 +0,0 @@ - - * @author Icinga Development Team - */ -// {{{ICINGA_LICENSE_HEADER}}} - -namespace Icinga\Form; - -/** - * Class that helps building and validating forms and offers a rudimentary - * data-binding mechanismn. - * - * The underlying form can be accessed by expicitly calling $builder->getForm() or - * by directly calling the forms method (which is, in case of populate() the preferred way) - * like: $builder->getElements() - * - * @method \Zend_Form_Element getElement(string $name) - * @method \Zend_Form addElement(\string $element, string $name = null, array $options = null) - * @method \Zend_Form setView(\Zend_View $view) - * @package Icinga\Form - */ -class Builder -{ - const CSRF_ID = "icinga_csrf_id"; - - /** - * @var \Zend_Form - */ - private $form; - - /** - * @var null - */ - private $boundModel = null; - - /** - * @var bool - */ - private $disableCSRF = false; - - /** - * Constructrs a new Formbuilder, containing an empty form if no - * $form parameter is given or the Zend form from the $form parameter. - * - * @param \Zend_Form $form The form to use with this Builder - * @param Array $options an optional array of Options: - * - CSRFProtection true to add a crsf token to the - * form (default), false to remove it - * - model An referenced array or object to use - * for value binding - **/ - public function __construct(\Zend_Form $form = null, array $options = array()) - { - if ($form === null) { - $myModel = array( - "username" => "", - "password" => "" - ); - - $form = new \Zend_Form(); - } - - $this->setZendForm($form); - - if (isset($options["CSRFProtection"])) { - $this->disableCSRF = !$options["CSRFProtection"]; - } - if (isset($options["model"])) { - $this->bindToModel($options["model"]); - } - } - - /** - * Setter for Zend_Form - * @param \Zend_Form $form - */ - public function setZendForm(\Zend_Form $form) - { - $this->form = $form; - } - - /** - * Getter for Zent_Form - * @return \Zend_Form - */ - public function getForm() - { - if (!$this->disableCSRF) { - $this->addCSRFFieldToForm(); - } - if (!$this->form) { - return new \Zend_Form(); - } - return $this->form; - } - - /** - * Add elements to form - * @param array $elements - */ - public function addElementsFromConfig(array $elements) - { - foreach ($elements as $key => $values) { - $this->addElement($values[0], $key, $values[1]); - } - } - - /** - * Quick add elements to a new builder instance - * @param array $elements - * @param array $options - * @return Builder - */ - public static function fromArray(array $elements, $options = array()) - { - $builder = new Builder(null, $options); - $builder->addElementsFromConfig($elements); - return $builder; - } - - /** - * Test that the form is valid - * - * @param array $data - * @return bool - */ - public function isValid(array $data = null) - { - if ($data === null) { - $data = $_POST; - } - return $this->hasValidToken() && $this->form->isValid($data); - } - - /** - * Test if the form was submitted - * @param string $btnName - * @return bool - */ - public function isSubmitted($btnName = 'submit') - { - $btn = $this->getElement($btnName); - if (!$btn || !isset($_POST[$btnName])) { - return false; - } - return $_POST[$btnName] === $btn->getLabel(); - } - - /** - * Render the form - * @return string - */ - public function render() - { - return $this->getForm()->render(); - } - - public function __toString() - { - return $this->getForm()->__toString(); - } - - /** - * Enable CSRF token field - */ - public function enableCSRF() - { - $this->disableCSRF = false; - } - - /** - * Disable CSRF token field - */ - public function disableCSRF() - { - $this->disableCSRF = true; - $this->form->removeElement(self::CSRF_ID); - $this->form->removeElement(self::CSRF_ID."_seed"); - } - - /** - * Add CSRF field to form - */ - private function addCSRFFieldToForm() - { - if (!$this->form || $this->disableCSRF || $this->form->getElement(self::CSRF_ID)) { - return; - } - list($seed, $token) = $this->getSeedTokenPair(); - - $this->form->addElement("hidden", self::CSRF_ID); - $this->form->getElement(self::CSRF_ID) - ->setValue($token) - ->setDecorators(array('ViewHelper')); - - $this->form->addElement("hidden", self::CSRF_ID."_seed"); - $this->form->getElement(self::CSRF_ID."_seed") - ->setValue($seed) - ->setDecorators(array('ViewHelper')); - - } - - /** - * Bind model to a form - * @param $model - */ - public function bindToModel(&$model) - { - $this->boundModel = &$model; - } - - /** - * Repopulate - */ - public function repopulate() - { - if (!empty($_POST)) { - $this->populate($_POST); - } - } - - /** - * Populate form - * @param $data - * @param bool $ignoreModel - * @throws \InvalidArgumentException - */ - public function populate($data, $ignoreModel = false) - { - if (is_array($data)) { - $this->form->populate($data); - } elseif (is_object($data)) { - $this->populateFromObject($data); - } else { - throw new \InvalidArgumentException("Builder::populate() expects and object or array, $data given"); - } - if ($this->boundModel === null || $ignoreModel) { - return; - } - $this->updateModel(); - - } - - /** - * Populate form object - * @param $data - */ - private function populateFromObject($data) - { - /** @var \Zend_Form_Element $element */ - - foreach ($this->form->getElements() as $name => $element) { - if (isset($data->$name)) { - $element->setValue($data->$name); - - } else { - $getter = "get".ucfirst($name); - if (method_exists($data, $getter)) { - $element->setValue($data->$getter()); - } - } - } - } - - /** - * Update model instance - */ - public function updateModel() - { - if (is_array($this->boundModel)) { - $this->updateArrayModel(); - } elseif (is_object($this->boundModel)) { - $this->updateObjectModel(); - } - } - - /** - * Updater for objects - */ - private function updateObjectModel() - { - /** @var \Zend_Form_Element $element */ - - foreach ($this->form->getElements() as $name => $element) { - if (isset($this->boundModel->$name)) { - $this->boundModel->$name = $element->getValue(); - } else { - $setter = "set".ucfirst($name); - if (method_exists($this->boundModel, $setter)) { - $this->boundModel->$setter($element->getValue()); - } - - } - } - } - - /** - * Updater for arrays - */ - private function updateArrayModel() - { - /** @var \Zend_Form_Element $element */ - - foreach ($this->form->getElements() as $name => $element) { - if (isset($this->boundModel[$name])) { - $this->boundModel[$name] = $element->getValue(); - } - } - } - - /** - * Synchronize model with form - */ - public function syncWithModel() - { - $this->populate($this->boundModel, true); - } - - /** - * Magic caller, pass through method calls to form - * @param $fn - * @param array $args - * @return mixed - * @throws \BadMethodCallException - */ - public function __call($fn, array $args) - { - if (method_exists($this->form, $fn)) { - return call_user_func_array(array($this->form, $fn), $args); - } else { - throw new \BadMethodCallException( - "Method $fn does not exist either ". - "in \Icinga\Form\Builder nor in Zend_Form" - ); - } - } - - - /** - * Whether the token parameter is valid - * - * @param int $maxAge Max allowed token age - * @param string $sessionId A specific session id (useful for tests?) - * - * @return bool - */ - public function hasValidToken($maxAge = 600, $sessionId = null) - { - if ($this->disableCSRF) { - return true; - } - - if ($this->getForm()->getElement(self::CSRF_ID) == null) { - return false; - } - - $sessionId = $sessionId ? $sessionId : session_id(); - $seed = $this->getForm()->getElement(self::CSRF_ID.'_seed')->getValue(); - - if (! is_numeric($seed)) { - return false; - } - - // Remove quantitized timestamp portion so maxAge applies - $seed -= (intval(time() / $maxAge) * $maxAge); - $token = $this->getElement(self::CSRF_ID)->getValue(); - return $token === hash('sha256', $sessionId . $seed); - } - - /** - * Get a new seed/token pair - * - * @param int $maxAge Max allowed token age - * @param string $sessionId A specific session id (useful for tests?) - * - * @return array - */ - public function getSeedTokenPair($maxAge = 600, $sessionId = null) - { - $sessionId = $sessionId ? $sessionId : session_id(); - $seed = mt_rand(); - $hash = hash('sha256', $sessionId . $seed); - - // Add quantitized timestamp portion to apply maxAge - $seed += (intval(time() / $maxAge) * $maxAge); - return array($seed, $hash); - } -} diff --git a/library/Icinga/Form/Form.php b/library/Icinga/Form/Form.php new file mode 100644 index 000000000..0956081ee --- /dev/null +++ b/library/Icinga/Form/Form.php @@ -0,0 +1,345 @@ + + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + +namespace Icinga\Form; + +use Icinga\Exception\ProgrammingError; + +abstract class Form extends \Zend_Form +{ + /** + * The form's request object + * @var null + */ + private $request = null; + + /** + * The underlying model for this form + * @var null + */ + private $boundModel = null; + + /** + * Whether this form should NOT add random generated "challenge" tokens that are associated + * with the user's current session in order to prevent Cross-Site Request Forgery (CSRF). + * It is the form's responsibility to verify the existence and correctness of this token + * @var bool + */ + private $tokenDisabled = false; + + /** + * Name of the CSRF token element (used to create non-colliding hashes) + * @var string + */ + private $tokenElementName = 'CSRFToken'; + + /** + * Time to live for the CRSF token + * @var int + */ + private $tokenTimeout = 300; + + /** + * @see Zend_Form::init + */ + public function init() + { + if (!$this->tokenDisabled) { + $this->initCsrfToken(); + } + $this->create(); + } + + /** + * Add elements to this form (used by extending classes) + */ + abstract public function create(); + + /** + * Apply a request object wherewith the form can work + * + * @param $request The request object of a session + * @return $this + */ + public function setRequest($request) + { + $this->request = $request; + return $this; + } + + /** + * Check if the form's values are valid + * + * If $data is not given, $_POST is used. + * + * @param array $data + * @return bool + */ + /* @TODO: isValid cannot be overwritten this way. The CSRF functionality needs to + * be implemented as separate Zend_Form_Element with its own validator. + * + public function isValid(array $data = null) + { + if ($data === null) { + $data = $_POST; + } + return $this->hasValidCsrfToken($this->tokenTimeout) && parent::isValid($data); + } + */ + + /** + * Check if the form was submitted + * + * @param string $elementName + * @return bool + */ + public function isSubmitted($elementName) + { + $element = $this->getElement($elementName); + if ($element) { + if ($this->request->isGet()) { + return $this->request->getQuery($elementName) === $element->getLabel(); + } elseif ($this->request->isPost()) { + return $this->request->getPost($elementName) === $element->getLabel(); + } + } + return false; + } + + /** + * Populate form with the given information + * + * $model might be an array or object where each key/property represents the name + * of an element. In case its a object it might also have getter for each element. + * (Naming: getExample) If $updateModel is false the underlying model, if any, + * will not be updated. + * + * @param array|object $model + * @param bool $updateModel + * @throws \InvalidArgumentException + */ + public function populate($model, $updateModel = true) + { + if (is_array($model)) { + parent::populate($model); + } elseif (is_object($model)) { + $this->populateFromObject($model); + } else { + throw new \InvalidArgumentException("Expected array or object. $model given"); + } + if ($updateModel && $this->boundModel !== null) { + $this->updateModel(); + } + } + + /** + * Populate form with the given object + * + * @param object $model + */ + private function populateFromObject($model) + { + foreach ($this->getElements() as $name => $element) { + if (isset($model->$name)) { + $element->setValue($model->$name); + } else { + $getter = 'get' . ucfirst($name); + if (method_exists($model, $getter)) { + $element->setValue($model->$getter()); + } + } + } + } + + /** + * Repopulate form with the current request + */ + public function repopulate() + { + if ($this->request->isPost()) { + $this->populate($this->request->getPost()); + } elseif ($this->request->isGet()) { + $this->populate($this->request->getQuery()); + } + } + + /** + * Bind a model to this form + * + * $model might be an array or object where each key/property represents + * the name of an element. In case its a object it might also have getter + * and setter for each element. (Naming: getExample, setExample) + * + * @param array|object $model + */ + public function bindToModel(&$model) + { + $this->boundModel = &$model; + } + + /** + * Synchronize form with model + * + * Feed the form's elements with default values by using the current model. + * + * @throws ProgrammingError + */ + public function syncWithModel() + { + if ($this->boundModel === null) { + throw new ProgrammingError('You need to bind a model to this form first'); + } + $this->populate($this->boundModel, true); + } + + /** + * Update model with the form's values + * + * This is the inverse of syncWithModel(). Feed the + * model with values from the form's elements. + * + * @throws ProgrammingError + */ + public function updateModel() + { + if (is_array($this->boundModel)) { + $this->updateArrayModel(); + } elseif (is_object($this->boundModel)) { + $this->updateObjectModel(); + } else { + throw new ProgrammingError('You need to bind a model to this form first'); + } + } + + /** + * Update model of type object + */ + private function updateObjectModel() + { + foreach ($this->getElements() as $name => $element) { + if (isset($this->boundModel->$name)) { + $this->boundModel->$name = $element->getValue(); + } else { + $setter = 'set' . ucfirst($name); + if (method_exists($this->boundModel, $setter)) { + $this->boundModel->$setter($element->getValue()); + } + } + } + } + + /** + * Update model of type array + */ + private function updateArrayModel() + { + foreach ($this->getElements() as $name => $element) { + if (isset($this->boundModel[$name])) { + $this->boundModel[$name] = $element->getValue(); + } + } + } + + /** + * Enable CSRF counter measure + */ + final public function enableCsrfToken() + { + $this->tokenDisabled = false; + } + + /** + * Disable CSRF counter measure and remove its field if already added + */ + final public function disableCsrfToken() + { + $this->tokenDisabled = true; + $this->removeElement($this->tokenElementName); + } + + /** + * Add CSRF counter measure field to form + */ + final public function initCsrfToken() + { + if ($this->tokenDisabled || $this->getElement($this->tokenElementName)) { + return; + } + list($seed, $token) = $this->generateCsrfToken($this->tokenTimeout); + + $this->addElement('hidden', $this->tokenElementName, array( + 'value' => sprintf('%s\|/%s', $seed, $token), + 'decorators' => array('ViewHelper') + ) + ); + } + + /** + * Check whether the form's CSRF token-field has a valid value + * + * @param int $maxAge Max allowed token age + * @param string $sessionId A specific session id + * + * @return bool + */ + final private function hasValidCsrfToken($maxAge, $sessionId = null) + { + if ($this->tokenDisabled) { + return true; + } + + if ($this->getElement($this->tokenElementName) === null) { + return false; + } + + $elementValue = $this->getElement($this->tokenElementName)->getValue(); + list($seed, $token) = explode($elementValue, '\|/'); + + if (!is_numeric($seed)) { + return false; + } + + $seed -= intval(time() / $maxAge) * $maxAge; + $sessionId = $sessionId ? $sessionId : session_id(); + return $token === hash('sha256', $sessionId . $seed); + } + + /** + * Generate a new (seed, token) pair + * + * @param int $maxAge Max allowed token age + * @param string $sessionId A specific session id + * + * @return array + */ + final private function generateCsrfToken($maxAge, $sessionId = null) + { + $sessionId = $sessionId ? $sessionId : session_id(); + $seed = mt_rand(); + $hash = hash('sha256', $sessionId . $seed); + $seed += intval(time() / $maxAge) * $maxAge; + return array($seed, $hash); + } +}