diff --git a/src/IDF/Plugin/SyncMonotone.php b/src/IDF/Plugin/SyncMonotone.php index ecbb301..6fa9aca 100644 --- a/src/IDF/Plugin/SyncMonotone.php +++ b/src/IDF/Plugin/SyncMonotone.php @@ -35,28 +35,39 @@ class IDF_Plugin_SyncMonotone $plug = new IDF_Plugin_SyncMonotone(); switch ($signal) { case 'IDF_Project::created': - $plug->processMonotoneCreate($params['project']); + $plug->processProjectCreate($params['project']); + break; + case 'IDF_Project::preDelete': + $plug->processProjectDelete($params['project']); + break; + case 'IDF_Key::postSave': + $plug->processKeyCreate($params['key']); + break; + case 'IDF_Key::preDelete': + $plug->processKeyDelete($params['key']); break; case 'mtnpostpush.php::run': - $plug->processSyncTimeline($params); + $plug->processSyncTimeline($params['project']); break; } } /** - * Four steps to setup a new monotone project: + * Initial steps to setup a new monotone project: * * 1) run mtn db init to initialize a new database underknees * 'mtn_repositories' * 2) create a new server key in the same directory - * 3) write monotonerc for access control - * 4) add the database as new local server in the usher configuration - * 5) reload the running usher instance so it acknowledges the new - * server + * 3) create a new client key for IDF and store it in the project conf + * 4) write monotonerc + * 5) add the database as new local server in the usher configuration + * 6) reload the running usher instance so it acknowledges the new server + * 7) create read-/write-permissions for the project and add all public + * keys to the project * * @param IDF_Project */ - function processMonotoneCreate($project) + function processProjectCreate($project) { if ($project->getConf()->getVal('scm') != 'mtn') { return; @@ -82,7 +93,7 @@ class IDF_Plugin_SyncMonotone __('Could not find mtn-post-push script "%s".'), $mtnpostpush )); } - + $shortname = $project->shortname; $projectpath = sprintf($projecttempl, $shortname); if (file_exists($projectpath)) { @@ -101,18 +112,8 @@ class IDF_Plugin_SyncMonotone // step 1) create a new database // $dbfile = $projectpath.'/database.mtn'; - $cmd = sprintf( - Pluf::f('mtn_path', 'mtn').' db init -d %s', - escapeshellarg($dbfile) - ); - $cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd; - $output = $return = null; - $ll = exec($cmd, $output, $return); - if ($return != 0) { - throw new IDF_Scm_Exception(sprintf( - __('The database file %s could not be created.'), $dbfile - )); - } + $cmd = sprintf('db init -d %s', escapeshellarg($dbfile)); + self::_mtn_exec($cmd); // // step 2) create a server key @@ -126,43 +127,92 @@ class IDF_Plugin_SyncMonotone $server = $parsed['host']; } - $keyname = $shortname.'-server@'.$server; - $cmd = sprintf( - Pluf::f('mtn_path', 'mtn').' au generate_key --confdir=%s %s ""', + $serverkey = $shortname.'-server@'.$server; + $cmd = sprintf('au generate_key --confdir=%s %s ""', escapeshellarg($projectpath), - escapeshellarg($keyname) + escapeshellarg($serverkey) ); + self::_mtn_exec($cmd); - $cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd; - $output = $return = null; - $ll = exec($cmd, $output, $return); - if ($return != 0) { - throw new IDF_Scm_Exception(sprintf( - __('The server key %s could not be created.'), $keyname - )); + // + // step 3) create a client key, and save it in IDF + // + $clientkey_hash = ''; + $monotonerc_tpl = 'monotonerc-noauth.tpl'; + + if (Pluf::f('mtn_remote_auth', true)) { + $monotonerc_tpl = 'monotonerc-auth.tpl'; + $keydir = Pluf::f('tmp_folder').'/mtn-client-keys'; + if (!file_exists($keydir)) { + if (!mkdir($keydir)) { + throw new IDF_Scm_Exception(sprintf( + __('The key directory %s could not be created.'), $keydir + )); + } + } + + $clientkey_name = $shortname.'-client@'.$server; + $cmd = sprintf('au generate_key --keydir=%s %s ""', + escapeshellarg($keydir), + escapeshellarg($clientkey_name) + ); + $keyinfo = self::_mtn_exec($cmd); + + $parsed_keyinfo = array(); + try { + $parsed_keyinfo = IDF_Scm_Monotone_BasicIO::parse($keyinfo); + } + catch (Exception $e) { + echo $e->getTraceAsString(); exit; + throw new IDF_Scm_Exception(sprintf( + __('Could not parse key information: %s'), $e->getMessage() + )); + } + + $clientkey_hash = $parsed_keyinfo[0][1]['hash']; + $clientkey_file = $keydir . '/' . $clientkey_name . '.' . $clientkey_hash; + $clientkey_data = file_get_contents($clientkey_file); + + $project->getConf()->setVal('mtn_client_key_name', $clientkey_name); + $project->getConf()->setVal('mtn_client_key_hash', $clientkey_hash); + $project->getConf()->setVal('mtn_client_key_data', $clientkey_data); + + // add the public client key to the server + $cmd = sprintf('au get_public_key --keydir=%s %s', + escapeshellarg($keydir), + escapeshellarg($clientkey_hash) + ); + $clientkey_pubdata = self::_mtn_exec($cmd); + + $cmd = sprintf('au put_public_key --confdir=%s %s', + escapeshellarg($projectpath), + escapeshellarg($clientkey_pubdata) + ); + $keyinfo = self::_mtn_exec($cmd); } // - // step 3) write monotonerc for access control - // FIXME: netsync access control is still missing! - // - $monotonerc = file_get_contents(dirname(__FILE__) . "/SyncMonotone/monotonerc.tpl"); + // step 4) write monotonerc + // + $monotonerc = file_get_contents( + dirname(__FILE__).'/SyncMonotone/'.$monotonerc_tpl + ); $monotonerc = str_replace( - array("%%MTNPOSTPUSH%%", "%%PROJECT%%"), - array($mtnpostpush, $shortname), + array('%%MTNPOSTPUSH%%', '%%PROJECT%%', '%%MTNCLIENTKEY%%'), + array($mtnpostpush, $shortname, $clientkey_hash), $monotonerc ); $rcfile = $projectpath.'/monotonerc'; - if (!file_put_contents($rcfile, $monotonerc, LOCK_EX)) { + if (file_put_contents($rcfile, $monotonerc, LOCK_EX) === false) { throw new IDF_Scm_Exception(sprintf( __('Could not write mtn configuration file "%s"'), $rcfile )); } // - // step 4) read in and append the usher config with the new server + // step 5) read in and append the usher config with the new server // $usher_rc = file_get_contents($usher_config); $parsed_config = array(); @@ -177,13 +227,10 @@ class IDF_Plugin_SyncMonotone } // ensure we haven't configured a server with this name already - foreach ($parsed_config as $stanzas) - { - foreach ($stanzas as $stanza_line) - { + foreach ($parsed_config as $stanzas) { + foreach ($stanzas as $stanza_line) { if ($stanza_line['key'] == 'server' && - $stanza_line['values'][0] == $shortname) - { + $stanza_line['values'][0] == $shortname) { throw new IDF_Scm_Exception(sprintf( __('usher configuration already contains a server '. 'entry named "%s"'), @@ -206,44 +253,463 @@ class IDF_Plugin_SyncMonotone // FIXME: more sanity - what happens on failing writes? we do not // have a backup copy of usher.conf around... - if (!file_put_contents($usher_config, $usher_rc, LOCK_EX)) { + if (file_put_contents($usher_config, $usher_rc, LOCK_EX) === false) { throw new IDF_Scm_Exception(sprintf( __('Could not write usher configuration file "%s"'), $usher_config )); } // - // step 5) reload usher to pick up the new configuration + // step 6) reload usher to pick up the new configuration // IDF_Scm_Monotone_Usher::reload(); + + // + // step 7) add public monotone keys for the project to + // read-permissions, write-permissions and the database + // + $mtn = IDF_Scm_Monotone::factory($project); + $stdio = $mtn->getStdio(); + + $auth_ids = self::getAuthorizedUserIds($project); + $key_ids = array(); + foreach ($auth_ids as $auth_id) { + $sql = new Pluf_SQL('user=%s', array($auth_id)); + $keys = Pluf::factory('IDF_Key')->getList(array('filter' => $sql->gen())); + foreach ($keys as $key) { + if ($key->getType() != 'mtn') + continue; + $stdio->exec(array('put_public_key', $key->content)); + $key_ids[] = $key->getMtnId(); + } + } + + $write_permissions = implode("\n", $key_ids); + $rcfile = $projectpath.'/write-permissions'; + if (file_put_contents($rcfile, $write_permissions, LOCK_EX) === false) { + throw new IDF_Scm_Exception(sprintf( + __('Could not write write-permissions file "%s"'), $rcfile + )); + } + + if ($project->private) { + $stanza = array( + array('key' => 'pattern', 'values' => array('*')), + ); + foreach ($key_ids as $key_id) + { + $stanza[] = array('key' => 'allow', 'values' => array($key_id)); + } + } + else { + $stanza = array( + array('key' => 'pattern', 'values' => array('*')), + array('key' => 'allow', 'values' => array('*')), + ); + } + $read_permissions = IDF_Scm_Monotone_BasicIO::compile(array($stanza)); + $rcfile = $projectpath.'/read-permissions'; + if (file_put_contents($rcfile, $read_permissions, LOCK_EX) === false) { + throw new IDF_Scm_Exception(sprintf( + __('Could not write read-permissions file "%s"'), $rcfile + )); + } + } + + /** + * Clean up after a mtn project was deleted + * + * @param IDF_Project + */ + public function processProjectDelete($project) + { + if ($project->getConf()->getVal('scm') != 'mtn') { + return; + } + + $usher_config = Pluf::f('mtn_usher_conf', false); + if (!$usher_config || !is_writable($usher_config)) { + throw new IDF_Scm_Exception( + '"mtn_usher_conf" does not exist or is not writable.' + ); + } + + $shortname = $project->shortname; + IDF_Scm_Monotone_Usher::killServer($shortname); + + $projecttempl = Pluf::f('mtn_repositories', false); + if ($projecttempl === false) { + throw new IDF_Scm_Exception( + '"mtn_repositories" must be defined in your configuration file.' + ); + } + + $usher_config = Pluf::f('mtn_usher_conf', false); + if (!$usher_config || !is_writable($usher_config)) { + throw new IDF_Scm_Exception( + '"mtn_usher_conf" does not exist or is not writable.' + ); + } + + $projectpath = sprintf($projecttempl, $shortname); + if (file_exists($projectpath)) { + if (!self::_delete_recursive($projectpath)) { + throw new IDF_Scm_Exception(sprintf( + __('One or more paths underknees %s could not be deleted.'), $projectpath + )); + } + } + + if (Pluf::f('mtn_remote_auth', true)) { + $keydir = Pluf::f('tmp_folder').'/mtn-client-keys'; + $keyname = $project->getConf()->getVal('mtn_client_key_name', false); + $keyhash = $project->getConf()->getVal('mtn_client_key_hash', false); + if ($keyname && $keyhash && + file_exists($keydir .'/'. $keyname . '.' . $keyhash)) { + if (!@unlink($keydir .'/'. $keyname . '.' . $keyhash)) { + throw new IDF_Scm_Exception(sprintf( + __('Could not delete client private key %s'), $keyname + )); + } + } + } + + $usher_rc = file_get_contents($usher_config); + $parsed_config = array(); + try { + $parsed_config = IDF_Scm_Monotone_BasicIO::parse($usher_rc); + } + catch (Exception $e) { + throw new IDF_Scm_Exception(sprintf( + __('Could not parse usher configuration in "%s": %s'), + $usher_config, $e->getMessage() + )); + } + + foreach ($parsed_config as $idx => $stanzas) { + foreach ($stanzas as $stanza_line) { + if ($stanza_line['key'] == 'server' && + $stanza_line['values'][0] == $shortname) { + unset($parsed_config[$idx]); + break; + } + } + } + + $usher_rc = IDF_Scm_Monotone_BasicIO::compile($parsed_config); + + // FIXME: more sanity - what happens on failing writes? we do not + // have a backup copy of usher.conf around... + if (file_put_contents($usher_config, $usher_rc, LOCK_EX) === false) { + throw new IDF_Scm_Exception(sprintf( + __('Could not write usher configuration file "%s"'), $usher_config + )); + } + + IDF_Scm_Monotone_Usher::reload(); + } + + /** + * Adds the (monotone) key to all monotone projects of this forge + * where the user of the key has write access to + */ + public function processKeyCreate($key) + { + if ($key->getType() != 'mtn') + return; + + $projecttempl = Pluf::f('mtn_repositories', false); + if ($projecttempl === false) { + throw new IDF_Scm_Exception( + '"mtn_repositories" must be defined in your configuration file.' + ); + } + + foreach (Pluf::factory('IDF_Project')->getList() as $project) { + $conf = new IDF_Conf(); + $conf->setProject($project); + $scm = $conf->getVal('scm', 'mtn'); + if ($scm != 'mtn') + continue; + + $shortname = $project->shortname; + $projectpath = sprintf($projecttempl, $shortname); + if (!file_exists($projectpath)) { + throw new IDF_Scm_Exception(sprintf( + __('The project path %s does not exists.'), $projectpath + )); + } + + $auth_ids = self::getAuthorizedUserIds($project); + if (!in_array($key->user, $auth_ids)) + continue; + + $mtn_key_id = $key->getMtnId(); + + // if the project is not defined as private, all people have + // read access already, so we don't need to write anything + // and we currently do not check if read-permissions really + // contains + // pattern "*" + // allow "*" + // which is the default for non-private projects + if ($project->private == true) { + $read_perms = file_get_contents($projectpath.'/read-permissions'); + $parsed_read_perms = array(); + try { + $parsed_read_perms = IDF_Scm_Monotone_BasicIO::parse($read_perms); + } + catch (Exception $e) { + throw new IDF_Scm_Exception(sprintf( + __('Could not parse read-permissions for project "%s": %s'), + $shortname, $e->getMessage() + )); + } + + $wildcard_section = null; + foreach ($parsed_read_perms as $stanzas) { + foreach ($stanzas as $stanza_line) { + if ($stanza_line['key'] == 'pattern' && + $stanza_line['values'][0] == '*') { + $wildcard_section =& $stanzas; + break; + } + } + } + + if ($wildcard_section == null) + { + $wildcard_section = array( + array('key' => 'pattern', 'values' => array('*')) + ); + $parsed_read_perms[] =& $wildcard_section; + } + + $key_found = false; + foreach ($wildcard_section as $line) + { + if ($line['key'] == 'allow' && $line['values'][0] == $mtn_key_id) { + $key_found = true; + break; + } + } + + if (!$key_found) { + $wildcard_section[] = array( + 'key' => 'allow', 'values' => array($mtn_key_id) + ); + } + + $read_perms = IDF_Scm_Monotone_BasicIO::compile($parsed_read_perms); + + if (file_put_contents($projectpath.'/read-permissions', + $read_perms, LOCK_EX) === false) { + throw new IDF_Scm_Exception(sprintf( + __('Could not write read-permissions for project "%s"'), $shortname + )); + } + } + + $write_perms = file_get_contents($projectpath.'/write-permissions'); + $lines = preg_split("/(\n|\r\n)/", $write_perms); + if (!in_array('*', $lines) && !in_array($mtn_key_id, $lines)) { + $lines[] = $mtn_key_id; + } + if (file_put_contents($projectpath.'/write-permissions', + implode("\n", $lines), LOCK_EX) === false) { + throw new IDF_Scm_Exception(sprintf( + __('Could not write write-permissions file for project "%s"'), + $shortname + )); + } + + $mtn = IDF_Scm_Monotone::factory($project); + $stdio = $mtn->getStdio(); + $stdio->exec(array('put_public_key', $key->content)); + } + } + + /** + * Removes the (monotone) key from all monotone projects of this forge + * where the user of the key has write access to + */ + public function processKeyDelete($key) + { + if ($key->getType() != 'mtn') + return; + + $projecttempl = Pluf::f('mtn_repositories', false); + if ($projecttempl === false) { + throw new IDF_Scm_Exception( + '"mtn_repositories" must be defined in your configuration file.' + ); + } + + foreach (Pluf::factory('IDF_Project')->getList() as $project) { + $conf = new IDF_Conf(); + $conf->setProject($project); + $scm = $conf->getVal('scm', 'mtn'); + if ($scm != 'mtn') + continue; + + $shortname = $project->shortname; + $projectpath = sprintf($projecttempl, $shortname); + if (!file_exists($projectpath)) { + throw new IDF_Scm_Exception(sprintf( + __('The project path %s does not exists.'), $projectpath + )); + } + + $auth_ids = self::getAuthorizedUserIds($project); + if (!in_array($key->user, $auth_ids)) + continue; + + $mtn_key_id = $key->getMtnId(); + + // if the project is not defined as private, all people have + // read access already, so we don't need to write anything + // and we currently do not check if read-permissions really + // contains + // pattern "*" + // allow "*" + // which is the default for non-private projects + if ($project->private === true) { + $read_perms = file_get_contents($projectpath.'/read-permissions'); + $parsed_read_perms = array(); + try { + $parsed_read_perms = IDF_Scm_Monotone_BasicIO::parse($read_perms); + } + catch (Exception $e) { + throw new IDF_Scm_Exception(sprintf( + __('Could not parse read-permissions for project "%s": %s'), + $shortname, $e->getMessage() + )); + } + + // while we add new keys only to an existing wild-card entry + // we remove dropped keys from all sections since the key + // should be simply unavailable for all of them + foreach ($parsed_read_perms as $stanzas) { + for ($i=0; $igetStdio(); + // if the public key did not sign any revisions, drop it from + // the database as well + if (strlen($stdio->exec(array('select', 'k:' . $mtn_key_id))) == 0) { + $stdio->exec(array('drop_public_key', $mtn_key_id)); + } + } + } + + private static function getAuthorizedUserIds($project) + { + $mem = $project->getMembershipData(); + $members = array_merge((array)$mem['members'], + (array)$mem['owners'], + (array)$mem['authorized']); + $userids = array(); + foreach ($members as $member) { + $userids[] = $member->id; + } + return $userids; } /** * Update the timeline after a push * */ - public function processSyncTimeline($params) + public function processSyncTimeline($project_name) { - $pname = $params['project']; try { - $project = IDF_Project::getOr404($pname); + $project = IDF_Project::getOr404($project_name); } catch (Pluf_HTTP_Error404 $e) { Pluf_Log::event(array( - 'IDF_Plugin_SyncMonotone::processSyncTimeline', + 'IDF_Plugin_SyncMonotone::processSyncTimeline', 'Project not found.', - array($pname, $params) + array($project_name, $params) )); return false; // Project not found } Pluf_Log::debug(array( - 'IDF_Plugin_SyncMonotone::processSyncTimeline', - 'Project found', $pname, $project->id + 'IDF_Plugin_SyncMonotone::processSyncTimeline', + 'Project found', $project_name, $project->id )); IDF_Scm::syncTimeline($project, true); Pluf_Log::event(array( 'IDF_Plugin_SyncMonotone::processSyncTimeline', - 'sync', array($pname, $project->id) + 'sync', array($project_name, $project->id) )); } + + private static function _mtn_exec($cmd) + { + $fullcmd = sprintf('%s %s %s', + Pluf::f('idf_exec_cmd_prefix', ''), + Pluf::f('mtn_path', 'mtn'), + $cmd + ); + + $output = $return = null; + exec($fullcmd, $output, $return); + if ($return != 0) { + throw new IDF_Scm_Exception(sprintf( + __('The command "%s" could not be executed.'), $cmd + )); + } + return implode("\n", $output); + } + + private static function _delete_recursive($path) + { + if (is_file($path)) { + return @unlink($path); + } + + if (is_dir($path)) { + $scan = glob(rtrim($path, '/') . '/*'); + $status = 0; + foreach ($scan as $subpath) { + $status |= self::_delete_recursive($subpath); + } + $status |= rmdir($path); + return $status; + } + } } diff --git a/src/IDF/Plugin/SyncMonotone/monotonerc-auth.tpl b/src/IDF/Plugin/SyncMonotone/monotonerc-auth.tpl new file mode 100644 index 0000000..daf3307 --- /dev/null +++ b/src/IDF/Plugin/SyncMonotone/monotonerc-auth.tpl @@ -0,0 +1,63 @@ +-- ***** BEGIN LICENSE BLOCK ***** +-- This file is part of InDefero, an open source project management application. +-- Copyright (C) 2010 Céondo Ltd and contributors. +-- +-- InDefero is free software; you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation; either version 2 of the License, or +-- (at your option) any later version. +-- +-- InDefero is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with this program; if not, write to the Free Software +-- Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +-- +-- ***** END LICENSE BLOCK ***** + +-- +-- controls the access rights for remote_stdio which is used by IDFs frontend +-- and other interested parties +-- +function get_remote_automate_permitted(key_identity, command, options) + if (key_identity.id == "%%MTNCLIENTKEY%%") then + return true + end + + return false +end + +-- +-- let IDF know of new arriving revisions to fill its timeline +-- +_idf_revs = {} +function note_netsync_start(session_id) + _idf_revs[session_id] = {} +end + +function note_netsync_revision_received(new_id, revision, certs, session_id) + table.insert(_idf_revs[session_id], new_id) +end + +function note_netsync_end (session_id, ...) + if table.getn(_idf_revs[session_id]) == 0 then + return + end + + local pin,pout,pid = spawn_pipe("%%MTNPOSTPUSH%%", "%%PROJECT%%"); + if pid == -1 then + print("could execute %%MTNPOSTPUSH%%") + return + end + + for _,r in ipairs(_idf_revs[session_id]) do + pin:write(r .. "\n") + end + pin:close() + + wait(pid) +end + diff --git a/src/IDF/Plugin/SyncMonotone/monotonerc-noauth.tpl b/src/IDF/Plugin/SyncMonotone/monotonerc-noauth.tpl new file mode 100644 index 0000000..c0c0050 --- /dev/null +++ b/src/IDF/Plugin/SyncMonotone/monotonerc-noauth.tpl @@ -0,0 +1,75 @@ +-- ***** BEGIN LICENSE BLOCK ***** +-- This file is part of InDefero, an open source project management application. +-- Copyright (C) 2010 Céondo Ltd and contributors. +-- +-- InDefero is free software; you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation; either version 2 of the License, or +-- (at your option) any later version. +-- +-- InDefero is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with this program; if not, write to the Free Software +-- Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +-- +-- ***** END LICENSE BLOCK ***** + +-- +-- controls the access rights for remote_stdio which is used by IDFs frontend +-- and other interested parties +-- +function get_remote_automate_permitted(key_identity, command, options) + local read_only_commands = { + "get_corresponding_path", "get_content_changed", "tags", "branches", + "common_ancestors", "packet_for_fdelta", "packet_for_fdata", + "packets_for_certs", "packet_for_rdata", "get_manifest_of", + "get_revision", "select", "graph", "children", "parents", "roots", + "leaves", "ancestry_difference", "toposort", "erase_ancestors", + "descendents", "ancestors", "heads", "get_file_of", "get_file", + "interface_version", "get_attributes", "content_diff", + "file_merge", "show_conflicts", "certs", "keys" + } + + for _,v in ipairs(read_only_commands) do + if (v == command[1]) then + return true + end + end + + return false +end + +-- +-- let IDF know of new arriving revisions to fill its timeline +-- +_idf_revs = {} +function note_netsync_start(session_id) + _idf_revs[session_id] = {} +end + +function note_netsync_revision_received(new_id, revision, certs, session_id) + table.insert(_idf_revs[session_id], new_id) +end + +function note_netsync_end (session_id, ...) + if table.getn(_idf_revs[session_id]) == 0 then + return + end + + local pin,pout,pid = spawn_pipe("%%MTNPOSTPUSH%%", "%%PROJECT%%"); + if pid == -1 then + print("could execute %%MTNPOSTPUSH%%") + return + end + + for _,r in ipairs(_idf_revs[session_id]) do + pin:write(r .. "\n") + end + pin:close() + + wait(pid) +end diff --git a/src/IDF/Plugin/SyncMonotone/monotonerc.tpl b/src/IDF/Plugin/SyncMonotone/monotonerc.tpl deleted file mode 100644 index 6c74813..0000000 --- a/src/IDF/Plugin/SyncMonotone/monotonerc.tpl +++ /dev/null @@ -1,71 +0,0 @@ --- ***** BEGIN LICENSE BLOCK ***** --- This file is part of InDefero, an open source project management application. --- Copyright (C) 2008 Céondo Ltd and contributors. --- --- InDefero is free software; you can redistribute it and/or modify --- it under the terms of the GNU General Public License as published by --- the Free Software Foundation; either version 2 of the License, or --- (at your option) any later version. --- --- InDefero is distributed in the hope that it will be useful, --- but WITHOUT ANY WARRANTY; without even the implied warranty of --- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the --- GNU General Public License for more details. --- --- You should have received a copy of the GNU General Public License --- along with this program; if not, write to the Free Software --- Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA --- --- ***** END LICENSE BLOCK ***** - --- --- controls the access rights for remote_stdio which is used by IDFs frontend --- -function get_remote_automate_permitted(key_identity, command, options) - local read_only_commands = { - "get_corresponding_path", "get_content_changed", "tags", "branches", - "common_ancestors", "packet_for_fdelta", "packet_for_fdata", - "packets_for_certs", "packet_for_rdata", "get_manifest_of", - "get_revision", "select", "graph", "children", "parents", "roots", - "leaves", "ancestry_difference", "toposort", "erase_ancestors", - "descendents", "ancestors", "heads", "get_file_of", "get_file", - "interface_version", "get_attributes", "content_diff", - "file_merge", "show_conflicts", "certs", "keys" - } - - for _,v in ipairs(read_only_commands) do - if (v == command[1]) then - return true - end - end - - return false -end - -_idf_revs = {} -function note_netsync_start(session_id) - _idf_revs[session_id] = {} -end - -function note_netsync_revision_received(new_id, revision, certs, session_id) - table.insert(_idf_revs[session_id], new_id) -end - -function note_netsync_end (session_id, ...) - if table.getn(_idf_revs[session_id]) == 0 then - return - end - - local pin,pout,pid = spawn_pipe("%%MTNPOSTPUSH%%", "%%PROJECT%%"); - if pid == -1 then - print("could execute %%MTNPOSTPUSH%%") - return - end - - for _,r in ipairs(_idf_revs[session_id]) do - pin:write(r .. "\n") - end - pin:close() - - wait(pid) -end diff --git a/src/IDF/Scm/Monotone/Stdio.php b/src/IDF/Scm/Monotone/Stdio.php index b447955..fa243d8 100644 --- a/src/IDF/Scm/Monotone/Stdio.php +++ b/src/IDF/Scm/Monotone/Stdio.php @@ -63,6 +63,55 @@ class IDF_Scm_Monotone_Stdio } /** + * Returns a string with additional options which are passed to + * an mtn instance connecting to remote databases + * + * @return string + */ + public function _getAuthOptions() + { + // no remote authentication - the simple case + if (!Pluf::f('mtn_remote_auth', true)) { + return '--key= '; + } + + $prjconf = $this->project->getConf(); + $name = $prjconf->getVal('mtn_client_key_name', false); + $hash = $prjconf->getVal('mtn_client_key_hash', false); + + if (!$name || !$hash) { + throw new IDF_Scm_Exception(sprintf( + __('Monotone client key name or hash not in project conf.') + )); + } + + $keydir = Pluf::f('tmp_folder').'/mtn-client-keys'; + if (!file_exists($keydir)) { + if (!mkdir($keydir)) { + throw new IDF_Scm_Exception(sprintf( + __('The key directory %s could not be created.'), $keydir + )); + } + } + + // in case somebody cleaned out the cache, we restore the key here + $keyfile = $keydir . '/' . $name .'.'. $hash; + if (!file_exists($keyfile)) { + $data = $prjconf->getVal('mtn_client_key_data'); + if (!file_put_contents($keyfile, $data, LOCK_EX)) { + throw new IDF_Scm_Exception(sprintf( + __('Could not write client key "%s"'), $keyfile + )); + } + } + + return sprintf('--keydir=%s --key=%s ', + escapeshellarg($keydir), + escapeshellarg($hash) + ); + } + + /** * Starts the stdio process and resets the command counter */ public function start() @@ -80,9 +129,8 @@ class IDF_Scm_Monotone_Stdio $cmd .= sprintf('%s ', escapeshellarg($opt)); } - // FIXME: we might want to add an option for anonymous / no key - // access, but upstream bug #30237 prevents that for now if ($remote_db_access) { + $cmd .= $this->_getAuthOptions(); $host = sprintf(Pluf::f('mtn_remote_url'), $this->project->shortname); $cmd .= sprintf('automate remote_stdio %s', escapeshellarg($host)); } @@ -104,7 +152,6 @@ class IDF_Scm_Monotone_Stdio ); $env = array('LANG' => 'en_US.UTF-8'); - $this->proc = proc_open($cmd, $descriptors, $this->pipes, null, $env); diff --git a/src/IDF/conf/idf.php-dist b/src/IDF/conf/idf.php-dist index 9339a5a..d4bcd5c 100644 --- a/src/IDF/conf/idf.php-dist +++ b/src/IDF/conf/idf.php-dist @@ -76,7 +76,7 @@ $cfg['svn_remote_url'] = 'http://localhost/svn/%s'; # Path to the monotone binary (you need mtn 0.99 or newer) $cfg['mtn_path'] = 'mtn'; # Additional options for the started monotone process -$cfg['mtn_opts'] = array('--no-workspace', '--no-standard-rcfiles', '--key='); +$cfg['mtn_opts'] = array('--no-workspace', '--no-standard-rcfiles'); # # You can setup monotone for use with indefero in several ways. The # two most-used should be: @@ -157,6 +157,19 @@ $cfg['mtn_remote_url'] = 'mtn://my-host.biz/%s'; # $cfg['mtn_db_access'] = 'remote'; # +# If true, each access to the database is authenticated with an auto-generated +# project key which is stored in the IDF project configuration +# ('mtn_client_key_*') and written out to $cfg['tmp_folder']/mtn-client-keys +# for its actual use. This key is then configured on the server to have +# full read / write access to all functions, while anonymous access can be +# completely disabled. +# If false, IDF tries to connect anonymously, without authentication, to +# the remote monotone server instance. In this case no project-specific +# keys are generated and the server must be configured to allow at least +# anonymous read access to the main functions. +# +$cfg['mtn_remote_auth'] = true; +# # If configured, this allows basic control of a running usher process # via the forge administration. The variable must point to the full (writable) # path of the usher configuration file which gets updated when new projects diff --git a/src/IDF/relations.php b/src/IDF/relations.php index 6734515..16dd8b4 100644 --- a/src/IDF/relations.php +++ b/src/IDF/relations.php @@ -88,6 +88,12 @@ Pluf_Signal::connect('gitpostupdate.php::run', # monotone synchronization Pluf_Signal::connect('IDF_Project::created', array('IDF_Plugin_SyncMonotone', 'entry')); +Pluf_Signal::connect('IDF_Project::preDelete', + array('IDF_Plugin_SyncMonotone', 'entry')); +Pluf_Signal::connect('IDF_Key::postSave', + array('IDF_Plugin_SyncMonotone', 'entry')); +Pluf_Signal::connect('IDF_Key::preDelete', + array('IDF_Plugin_SyncMonotone', 'entry')); Pluf_Signal::connect('phppostpush.php::run', array('IDF_Plugin_SyncMonotone', 'entry'));