From e47d51d14ce6b97bfa9c244e41fbef9c9d3582fb Mon Sep 17 00:00:00 2001 From: Thomas Keller Date: Wed, 11 Aug 2010 23:48:09 +0200 Subject: [PATCH] Add the possibility to save mtn public keys per user * src/IDF/Key.php: new column "type" which is either "ssh" or "mtn"; utility functions to query the mtn key name and id as well as all available key types for the current IDF installation * src/IDF/Migrations/16KeyType.php: needed migration script * src/IDF/Plugin/SyncGit/Cron.php: ensure only SSH keys are handled * adapt forms and templates accordingly --- src/IDF/Form/Admin/UserCreate.php | 40 +++-- src/IDF/Form/UserAccount.php | 140 +++++++++++++----- src/IDF/Key.php | 76 +++++++++- src/IDF/Migrations/16KeyType.php | 55 +++++++ src/IDF/Plugin/SyncGit/Cron.php | 4 +- src/IDF/Views/User.php | 4 +- .../templates/idf/gadmin/users/create.html | 12 +- src/IDF/templates/idf/user/myaccount.html | 14 +- 8 files changed, 274 insertions(+), 71 deletions(-) create mode 100644 src/IDF/Migrations/16KeyType.php diff --git a/src/IDF/Form/Admin/UserCreate.php b/src/IDF/Form/Admin/UserCreate.php index f7665e4..1e63073 100644 --- a/src/IDF/Form/Admin/UserCreate.php +++ b/src/IDF/Form/Admin/UserCreate.php @@ -77,21 +77,27 @@ class IDF_Form_Admin_UserCreate extends Pluf_Form 'initial' => '', 'widget' => 'Pluf_Form_Widget_SelectInput', 'widget_attrs' => array( - 'choices' => + 'choices' => Pluf_L10n::getInstalledLanguages() ), )); - $this->fields['ssh_key'] = new Pluf_Form_Field_Varchar( + $this->fields['public_key'] = new Pluf_Form_Field_Varchar( array('required' => false, - 'label' => __('Add a public SSH key'), + 'label' => __('Add a public key'), 'initial' => '', 'widget_attrs' => array('rows' => 3, 'cols' => 40), 'widget' => 'Pluf_Form_Widget_TextareaInput', 'help_text' => __('Be careful to provide the public key and not the private key!') )); - + $this->fields['public_key_type'] = new Pluf_Form_Field_Varchar( + array('required' => true, + 'label' => __('Key type'), + 'initial' => 'ssh', + 'widget_attrs' => array('choices' => IDF_Key::getAvailableKeyTypes()), + 'widget' => 'Pluf_Form_Widget_SelectInput', + )); } @@ -138,10 +144,11 @@ class IDF_Form_Admin_UserCreate extends Pluf_Form Pluf_Signal::send('Pluf_User::passwordUpdated', 'IDF_Form_Admin_UserCreate', $params); // Create the ssh key as needed - if ('' !== $this->cleaned_data['ssh_key']) { + if ('' !== $this->cleaned_data['public_key']) { $key = new IDF_Key(); $key->user = $user; - $key->content = $this->cleaned_data['ssh_key']; + $key->content = $this->cleaned_data['public_key']; + $key->type = $this->cleaned_data['public_key_type']; $key->create(); } // Send an email to the user with the password @@ -162,16 +169,11 @@ class IDF_Form_Admin_UserCreate extends Pluf_Form return $user; } - function clean_ssh_key() - { - return IDF_Form_UserAccount::checkSshKey($this->cleaned_data['ssh_key']); - } - function clean_last_name() { $last_name = trim($this->cleaned_data['last_name']); if ($last_name == mb_strtoupper($last_name)) { - return mb_convert_case(mb_strtolower($last_name), + return mb_convert_case(mb_strtolower($last_name), MB_CASE_TITLE, 'UTF-8'); } return $last_name; @@ -181,7 +183,7 @@ class IDF_Form_Admin_UserCreate extends Pluf_Form { $first_name = trim($this->cleaned_data['first_name']); if ($first_name == mb_strtoupper($first_name)) { - return mb_convert_case(mb_strtolower($first_name), + return mb_convert_case(mb_strtolower($first_name), MB_CASE_TITLE, 'UTF-8'); } return $first_name; @@ -211,4 +213,16 @@ class IDF_Form_Admin_UserCreate extends Pluf_Form } return $this->cleaned_data['login']; } + + /** + * Checks whether any given public key is valid + */ + public function clean() + { + $this->cleaned_data['public_key'] = + IDF_Form_UserAccount::checkPublicKey($this->cleaned_data['public_key'], + $this->cleaned_data['public_key_type']); + + return $this->cleaned_data; + } } diff --git a/src/IDF/Form/UserAccount.php b/src/IDF/Form/UserAccount.php index 7954f7e..8e57199 100644 --- a/src/IDF/Form/UserAccount.php +++ b/src/IDF/Form/UserAccount.php @@ -65,7 +65,7 @@ class IDF_Form_UserAccount extends Pluf_Form 'initial' => $this->user->language, 'widget' => 'Pluf_Form_Widget_SelectInput', 'widget_attrs' => array( - 'choices' => + 'choices' => Pluf_L10n::getInstalledLanguages() ), )); @@ -92,16 +92,22 @@ class IDF_Form_UserAccount extends Pluf_Form ), )); - $this->fields['ssh_key'] = new Pluf_Form_Field_Varchar( + $this->fields['public_key'] = new Pluf_Form_Field_Varchar( array('required' => false, - 'label' => __('Add a public SSH key'), + 'label' => __('Add a public key'), 'initial' => '', 'widget_attrs' => array('rows' => 3, 'cols' => 40), 'widget' => 'Pluf_Form_Widget_TextareaInput', 'help_text' => __('Be careful to provide your public key and not your private key!') )); - + $this->fields['public_key_type'] = new Pluf_Form_Field_Varchar( + array('required' => true, + 'label' => __('Key type'), + 'initial' => 'ssh', + 'widget_attrs' => array('choices' => IDF_Key::getAvailableKeyTypes()), + 'widget' => 'Pluf_Form_Widget_SelectInput', + )); } @@ -151,10 +157,11 @@ class IDF_Form_UserAccount extends Pluf_Form } $this->user->setFromFormData($this->cleaned_data); // Add key as needed. - if ('' !== $this->cleaned_data['ssh_key']) { + if ('' !== $this->cleaned_data['public_key']) { $key = new IDF_Key(); $key->user = $this->user; - $key->content = $this->cleaned_data['ssh_key']; + $key->content = $this->cleaned_data['public_key']; + $key->type = $this->cleaned_data['public_key_type']; if ($commit) { $key->create(); } @@ -190,61 +197,111 @@ class IDF_Form_UserAccount extends Pluf_Form } /** - * Check an ssh key. + * Check arbitrary public keys. * * It will throw a Pluf_Form_Invalid exception if it cannot * validate the key. * * @param $key string The key + * @param $key string The type ('ssh' or 'mtn') * @param $user int The user id of the user of the key (0) * @return string The clean key */ - public static function checkSshKey($key, $user=0) + public static function checkPublicKey($key, $type, $user=0) { $key = trim($key); - if (strlen($key) == 0) { + if (strlen($key) == 0) + { return ''; } - $key = str_replace(array("\n", "\r"), '', $key); - if (!preg_match('#^ssh\-[a-z]{3}\s(\S+)\s\S+$#', $key, $matches)) { - throw new Pluf_Form_Invalid(__('The format of the key is not valid. It must start with ssh-dss or ssh-rsa, a long string on a single line and at the end a comment.')); - } - if (Pluf::f('idf_strong_key_check', false)) { - $tmpfile = Pluf::f('tmp_folder', '/tmp').'/'.$user.'-key'; - file_put_contents($tmpfile, $key, LOCK_EX); - $cmd = Pluf::f('idf_exec_cmd_prefix', ''). - 'ssh-keygen -l -f '.escapeshellarg($tmpfile).' > /dev/null 2>&1'; - exec($cmd, $out, $return); - unlink($tmpfile); - if ($return != 0) { - throw new Pluf_Form_Invalid(__('Please check the key as it does not appears to be a valid key.')); + + if ($type == 'ssh') + { + $key = str_replace(array("\n", "\r"), '', $key); + if (!preg_match('#^ssh\-[a-z]{3}\s\S+\s\S+$#', $key)) + { + throw new Pluf_Form_Invalid( + __('The format of the key is not valid. It must start '. + 'with ssh-dss or ssh-rsa, a long string on a single '. + 'line and at the end a comment.') + ); + } + if (Pluf::f('idf_strong_key_check', false)) + { + $tmpfile = Pluf::f('tmp_folder', '/tmp').'/'.$user.'-key'; + file_put_contents($tmpfile, $key, LOCK_EX); + $cmd = Pluf::f('idf_exec_cmd_prefix', ''). + 'ssh-keygen -l -f '.escapeshellarg($tmpfile).' > /dev/null 2>&1'; + exec($cmd, $out, $return); + unlink($tmpfile); + if ($return != 0) + { + throw new Pluf_Form_Invalid( + __('Please check the key as it does not appears '. + 'to be a valid key.') + ); + } } } + else if ($type == 'mtn') + { + if (!preg_match('#^\[pubkey [^\]]+\]\s*\S+\s*\[end\]$#', $key)) + { + throw new Pluf_Form_Invalid( + __('The format of the key is not valid. It must start '. + 'with [pubkey KEYNAME], contain a long string on a single '. + 'line and end with [end] in the final third line.') + ); + } + if (Pluf::f('idf_strong_key_check', false)) + { + // if monotone can read it, it should be valid + $mtn_opts = implode(' ', Pluf::f('mtn_opts', array())); + $cmd = Pluf::f('idf_exec_cmd_prefix', ''). + sprintf('%s %s -d :memory: read >/tmp/php-out 2>&1', + Pluf::f('mtn_path', 'mtn'), $mtn_opts); + $fp = popen($cmd, 'w'); + fwrite($fp, $key); + $return = pclose($fp); + + if ($return != 0) + { + throw new Pluf_Form_Invalid( + __('Please check the key as it does not appears '. + 'to be a valid key.') + ); + } + } + } + else + { + throw new Pluf_Form_Invalid(__('Unknown key type')); + } + // If $user, then check if not the same key stored - if ($user) { + if ($user) + { $ruser = Pluf::factory('Pluf_User', $user); - if ($ruser->id > 0) { - $sql = new Pluf_SQL('content=%s', array($key)); + if ($ruser->id > 0) + { + $sql = new Pluf_SQL('content=%s AND type=%s', array($key, $type)); $keys = Pluf::factory('IDF_Key')->getList(array('filter' => $sql->gen())); - if (count($keys) > 0) { - throw new Pluf_Form_Invalid(__('You already have uploaded this SSH key.')); + if (count($keys) > 0) + { + throw new Pluf_Form_Invalid( + __('You already have uploaded this key.') + ); } } } return $key; } - function clean_ssh_key() - { - return self::checkSshKey($this->cleaned_data['ssh_key'], - $this->user->id); - } - function clean_last_name() { $last_name = trim($this->cleaned_data['last_name']); if ($last_name == mb_strtoupper($last_name)) { - return mb_convert_case(mb_strtolower($last_name), + return mb_convert_case(mb_strtolower($last_name), MB_CASE_TITLE, 'UTF-8'); } return $last_name; @@ -254,7 +311,7 @@ class IDF_Form_UserAccount extends Pluf_Form { $first_name = trim($this->cleaned_data['first_name']); if ($first_name == mb_strtoupper($first_name)) { - return mb_convert_case(mb_strtolower($first_name), + return mb_convert_case(mb_strtolower($first_name), MB_CASE_TITLE, 'UTF-8'); } return $first_name; @@ -264,7 +321,7 @@ class IDF_Form_UserAccount extends Pluf_Form { $this->cleaned_data['email'] = mb_strtolower(trim($this->cleaned_data['email'])); $guser = new Pluf_User(); - $sql = new Pluf_SQL('email=%s AND id!=%s', + $sql = new Pluf_SQL('email=%s AND id!=%s', array($this->cleaned_data['email'], $this->user->id)); if ($guser->getCount(array('filter' => $sql->gen())) > 0) { throw new Pluf_Form_Invalid(sprintf(__('The email "%s" is already used.'), $this->cleaned_data['email'])); @@ -273,11 +330,12 @@ class IDF_Form_UserAccount extends Pluf_Form } /** - * Check to see if the 2 passwords are the same. + * Check to see if the 2 passwords are the same and if any + * given public key is valid */ public function clean() { - if (!isset($this->errors['password']) + if (!isset($this->errors['password']) && !isset($this->errors['password2'])) { $password1 = $this->cleaned_data['password']; $password2 = $this->cleaned_data['password2']; @@ -285,6 +343,12 @@ class IDF_Form_UserAccount extends Pluf_Form throw new Pluf_Form_Invalid(__('The passwords do not match. Please give them again.')); } } + + $this->cleaned_data['public_key'] = + self::checkPublicKey($this->cleaned_data['public_key'], + $this->cleaned_data['public_key_type'], + $this->user->id); + return $this->cleaned_data; } } diff --git a/src/IDF/Key.php b/src/IDF/Key.php index 5cc80c3..3fcdd79 100644 --- a/src/IDF/Key.php +++ b/src/IDF/Key.php @@ -22,7 +22,7 @@ # ***** END LICENSE BLOCK ***** */ /** - * Storage of the SSH keys. + * Storage of the public keys (ssh or monotone). * */ class IDF_Key extends Pluf_Model @@ -39,9 +39,9 @@ class IDF_Key extends Pluf_Model array( 'type' => 'Pluf_DB_Field_Sequence', //It is automatically added. - 'blank' => true, + 'blank' => true, ), - 'user' => + 'user' => array( 'type' => 'Pluf_DB_Field_Foreignkey', 'model' => 'Pluf_User', @@ -52,14 +52,21 @@ class IDF_Key extends Pluf_Model array( 'type' => 'Pluf_DB_Field_Text', 'blank' => false, - 'verbose' => __('ssh key'), + 'verbose' => __('public key'), + ), + 'type' => + array( + 'type' => 'Pluf_DB_Field_Varchar', + 'size' => 3, + 'blank' => false, + 'verbose' => __('key type'), ), ); // WARNING: Not using getSqlTable on the Pluf_User object to // avoid recursion. - $t_users = $this->_con->pfx.'users'; + $t_users = $this->_con->pfx.'users'; $this->_a['views'] = array( - 'join_user' => + 'join_user' => array( 'join' => 'LEFT JOIN '.$t_users .' ON '.$t_users.'.id='.$this->_con->qn('user'), @@ -75,6 +82,46 @@ class IDF_Key extends Pluf_Model return Pluf_Template::markSafe(Pluf_esc(substr($this->content, 0, 25)).' [...] '.Pluf_esc(substr($this->content, -55))); } + private function parseMonotoneKeyData() + { + if ($this->type != "mtn") + throw new IDF_Exception("key is not a monotone key type"); + + preg_match("#^\[pubkey ([^\]]+)\]\s*(\S+)\s*\[end\]$#", $this->content, $m); + if (count($m) != 3) + throw new IDF_Exception("invalid key data detected"); + + return array($m[1], $m[2]); + } + + /** + * Returns the key name of the key, i.e. most of the time the email + * address, which not neccessarily has to be unique across a project. + * + * @return string + */ + function getMonotoneKeyName() + { + list($keyName, ) = $this->parseMonotoneKeyData(); + return $keyName; + } + + /** + * This function should be used to calculate the key id from the + * public key hash for authentication purposes. This avoids clashes + * in case the key name is not unique across the project + * + * And yes, this is actually how monotone itself calculates the key + * id... + * + * @return string + */ + function getMonotoneKeyId() + { + list($keyName, $keyData) = $this->parseMonotoneKeyData(); + return sha1($keyName.":".$keyData); + } + function postSave($create=false) { /** @@ -89,7 +136,7 @@ class IDF_Key extends Pluf_Model * [description] * * This signal allows an application to perform special - * operations after the saving of a SSH Key. + * operations after the saving of a public Key. * * [parameters] * @@ -128,4 +175,19 @@ class IDF_Key extends Pluf_Model 'IDF_Key', $params); } + /** + * Returns an associative array with available key types for this + * idf installation, ready for consumption for a