* configure whether or not to set remote client authentication for IDF -> remote_stdio
* hook into IDF_Project::preDelete, IDF_Key::postSave and IDF_Key::preDelete * this is all not quite finished, but a big leap forward to completion
This commit is contained in:
parent
bb13722a2f
commit
0f9f337e66
@ -35,28 +35,39 @@ class IDF_Plugin_SyncMonotone
|
|||||||
$plug = new IDF_Plugin_SyncMonotone();
|
$plug = new IDF_Plugin_SyncMonotone();
|
||||||
switch ($signal) {
|
switch ($signal) {
|
||||||
case 'IDF_Project::created':
|
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;
|
break;
|
||||||
case 'mtnpostpush.php::run':
|
case 'mtnpostpush.php::run':
|
||||||
$plug->processSyncTimeline($params);
|
$plug->processSyncTimeline($params['project']);
|
||||||
break;
|
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
|
* 1) run mtn db init to initialize a new database underknees
|
||||||
* 'mtn_repositories'
|
* 'mtn_repositories'
|
||||||
* 2) create a new server key in the same directory
|
* 2) create a new server key in the same directory
|
||||||
* 3) write monotonerc for access control
|
* 3) create a new client key for IDF and store it in the project conf
|
||||||
* 4) add the database as new local server in the usher configuration
|
* 4) write monotonerc
|
||||||
* 5) reload the running usher instance so it acknowledges the new
|
* 5) add the database as new local server in the usher configuration
|
||||||
* server
|
* 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
|
* @param IDF_Project
|
||||||
*/
|
*/
|
||||||
function processMonotoneCreate($project)
|
function processProjectCreate($project)
|
||||||
{
|
{
|
||||||
if ($project->getConf()->getVal('scm') != 'mtn') {
|
if ($project->getConf()->getVal('scm') != 'mtn') {
|
||||||
return;
|
return;
|
||||||
@ -101,18 +112,8 @@ class IDF_Plugin_SyncMonotone
|
|||||||
// step 1) create a new database
|
// step 1) create a new database
|
||||||
//
|
//
|
||||||
$dbfile = $projectpath.'/database.mtn';
|
$dbfile = $projectpath.'/database.mtn';
|
||||||
$cmd = sprintf(
|
$cmd = sprintf('db init -d %s', escapeshellarg($dbfile));
|
||||||
Pluf::f('mtn_path', 'mtn').' db init -d %s',
|
self::_mtn_exec($cmd);
|
||||||
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
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// step 2) create a server key
|
// step 2) create a server key
|
||||||
@ -126,43 +127,92 @@ class IDF_Plugin_SyncMonotone
|
|||||||
$server = $parsed['host'];
|
$server = $parsed['host'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$keyname = $shortname.'-server@'.$server;
|
$serverkey = $shortname.'-server@'.$server;
|
||||||
$cmd = sprintf(
|
$cmd = sprintf('au generate_key --confdir=%s %s ""',
|
||||||
Pluf::f('mtn_path', 'mtn').' au generate_key --confdir=%s %s ""',
|
|
||||||
escapeshellarg($projectpath),
|
escapeshellarg($projectpath),
|
||||||
escapeshellarg($keyname)
|
escapeshellarg($serverkey)
|
||||||
);
|
);
|
||||||
|
self::_mtn_exec($cmd);
|
||||||
|
|
||||||
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
|
//
|
||||||
$output = $return = null;
|
// step 3) create a client key, and save it in IDF
|
||||||
$ll = exec($cmd, $output, $return);
|
//
|
||||||
if ($return != 0) {
|
$clientkey_hash = '';
|
||||||
throw new IDF_Scm_Exception(sprintf(
|
$monotonerc_tpl = 'monotonerc-noauth.tpl';
|
||||||
__('The server key %s could not be created.'), $keyname
|
|
||||||
));
|
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
|
// step 4) write monotonerc
|
||||||
// FIXME: netsync access control is still missing!
|
|
||||||
//
|
//
|
||||||
$monotonerc = file_get_contents(dirname(__FILE__) . "/SyncMonotone/monotonerc.tpl");
|
$monotonerc = file_get_contents(
|
||||||
|
dirname(__FILE__).'/SyncMonotone/'.$monotonerc_tpl
|
||||||
|
);
|
||||||
$monotonerc = str_replace(
|
$monotonerc = str_replace(
|
||||||
array("%%MTNPOSTPUSH%%", "%%PROJECT%%"),
|
array('%%MTNPOSTPUSH%%', '%%PROJECT%%', '%%MTNCLIENTKEY%%'),
|
||||||
array($mtnpostpush, $shortname),
|
array($mtnpostpush, $shortname, $clientkey_hash),
|
||||||
$monotonerc
|
$monotonerc
|
||||||
);
|
);
|
||||||
|
|
||||||
$rcfile = $projectpath.'/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(
|
throw new IDF_Scm_Exception(sprintf(
|
||||||
__('Could not write mtn configuration file "%s"'), $rcfile
|
__('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);
|
$usher_rc = file_get_contents($usher_config);
|
||||||
$parsed_config = array();
|
$parsed_config = array();
|
||||||
@ -177,13 +227,10 @@ class IDF_Plugin_SyncMonotone
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ensure we haven't configured a server with this name already
|
// ensure we haven't configured a server with this name already
|
||||||
foreach ($parsed_config as $stanzas)
|
foreach ($parsed_config as $stanzas) {
|
||||||
{
|
foreach ($stanzas as $stanza_line) {
|
||||||
foreach ($stanzas as $stanza_line)
|
|
||||||
{
|
|
||||||
if ($stanza_line['key'] == 'server' &&
|
if ($stanza_line['key'] == 'server' &&
|
||||||
$stanza_line['values'][0] == $shortname)
|
$stanza_line['values'][0] == $shortname) {
|
||||||
{
|
|
||||||
throw new IDF_Scm_Exception(sprintf(
|
throw new IDF_Scm_Exception(sprintf(
|
||||||
__('usher configuration already contains a server '.
|
__('usher configuration already contains a server '.
|
||||||
'entry named "%s"'),
|
'entry named "%s"'),
|
||||||
@ -206,44 +253,463 @@ class IDF_Plugin_SyncMonotone
|
|||||||
|
|
||||||
// FIXME: more sanity - what happens on failing writes? we do not
|
// FIXME: more sanity - what happens on failing writes? we do not
|
||||||
// have a backup copy of usher.conf around...
|
// 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(
|
throw new IDF_Scm_Exception(sprintf(
|
||||||
__('Could not write usher configuration file "%s"'), $usher_config
|
__('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();
|
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; $i<count($stanzas); ) {
|
||||||
|
if ($stanzas[$i]['key'] == 'allow' &&
|
||||||
|
$stanzas[$i]['values'][0] == $mtn_key_id) {
|
||||||
|
unset($stanzas[$i]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
++$i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$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);
|
||||||
|
for ($i=0; $i<count($lines); ) {
|
||||||
|
if ($lines[$i] == $mtn_key_id) {
|
||||||
|
unset($lines[$i]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
++$i;
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
// 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
|
* Update the timeline after a push
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public function processSyncTimeline($params)
|
public function processSyncTimeline($project_name)
|
||||||
{
|
{
|
||||||
$pname = $params['project'];
|
|
||||||
try {
|
try {
|
||||||
$project = IDF_Project::getOr404($pname);
|
$project = IDF_Project::getOr404($project_name);
|
||||||
} catch (Pluf_HTTP_Error404 $e) {
|
} catch (Pluf_HTTP_Error404 $e) {
|
||||||
Pluf_Log::event(array(
|
Pluf_Log::event(array(
|
||||||
'IDF_Plugin_SyncMonotone::processSyncTimeline',
|
'IDF_Plugin_SyncMonotone::processSyncTimeline',
|
||||||
'Project not found.',
|
'Project not found.',
|
||||||
array($pname, $params)
|
array($project_name, $params)
|
||||||
));
|
));
|
||||||
return false; // Project not found
|
return false; // Project not found
|
||||||
}
|
}
|
||||||
|
|
||||||
Pluf_Log::debug(array(
|
Pluf_Log::debug(array(
|
||||||
'IDF_Plugin_SyncMonotone::processSyncTimeline',
|
'IDF_Plugin_SyncMonotone::processSyncTimeline',
|
||||||
'Project found', $pname, $project->id
|
'Project found', $project_name, $project->id
|
||||||
));
|
));
|
||||||
IDF_Scm::syncTimeline($project, true);
|
IDF_Scm::syncTimeline($project, true);
|
||||||
Pluf_Log::event(array(
|
Pluf_Log::event(array(
|
||||||
'IDF_Plugin_SyncMonotone::processSyncTimeline',
|
'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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
63
src/IDF/Plugin/SyncMonotone/monotonerc-auth.tpl
Normal file
63
src/IDF/Plugin/SyncMonotone/monotonerc-auth.tpl
Normal file
@ -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
|
||||||
|
|
@ -1,6 +1,6 @@
|
|||||||
-- ***** BEGIN LICENSE BLOCK *****
|
-- ***** BEGIN LICENSE BLOCK *****
|
||||||
-- This file is part of InDefero, an open source project management application.
|
-- This file is part of InDefero, an open source project management application.
|
||||||
-- Copyright (C) 2008 Céondo Ltd and contributors.
|
-- Copyright (C) 2010 Céondo Ltd and contributors.
|
||||||
--
|
--
|
||||||
-- InDefero is free software; you can redistribute it and/or modify
|
-- InDefero is free software; you can redistribute it and/or modify
|
||||||
-- it under the terms of the GNU General Public License as published by
|
-- it under the terms of the GNU General Public License as published by
|
||||||
@ -20,6 +20,7 @@
|
|||||||
|
|
||||||
--
|
--
|
||||||
-- controls the access rights for remote_stdio which is used by IDFs frontend
|
-- 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)
|
function get_remote_automate_permitted(key_identity, command, options)
|
||||||
local read_only_commands = {
|
local read_only_commands = {
|
||||||
@ -42,6 +43,9 @@ function get_remote_automate_permitted(key_identity, command, options)
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--
|
||||||
|
-- let IDF know of new arriving revisions to fill its timeline
|
||||||
|
--
|
||||||
_idf_revs = {}
|
_idf_revs = {}
|
||||||
function note_netsync_start(session_id)
|
function note_netsync_start(session_id)
|
||||||
_idf_revs[session_id] = {}
|
_idf_revs[session_id] = {}
|
@ -62,6 +62,55 @@ class IDF_Scm_Monotone_Stdio
|
|||||||
$this->stop();
|
$this->stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
* Starts the stdio process and resets the command counter
|
||||||
*/
|
*/
|
||||||
@ -80,9 +129,8 @@ class IDF_Scm_Monotone_Stdio
|
|||||||
$cmd .= sprintf('%s ', escapeshellarg($opt));
|
$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) {
|
if ($remote_db_access) {
|
||||||
|
$cmd .= $this->_getAuthOptions();
|
||||||
$host = sprintf(Pluf::f('mtn_remote_url'), $this->project->shortname);
|
$host = sprintf(Pluf::f('mtn_remote_url'), $this->project->shortname);
|
||||||
$cmd .= sprintf('automate remote_stdio %s', escapeshellarg($host));
|
$cmd .= sprintf('automate remote_stdio %s', escapeshellarg($host));
|
||||||
}
|
}
|
||||||
@ -104,7 +152,6 @@ class IDF_Scm_Monotone_Stdio
|
|||||||
);
|
);
|
||||||
|
|
||||||
$env = array('LANG' => 'en_US.UTF-8');
|
$env = array('LANG' => 'en_US.UTF-8');
|
||||||
|
|
||||||
$this->proc = proc_open($cmd, $descriptors, $this->pipes,
|
$this->proc = proc_open($cmd, $descriptors, $this->pipes,
|
||||||
null, $env);
|
null, $env);
|
||||||
|
|
||||||
|
@ -76,7 +76,7 @@ $cfg['svn_remote_url'] = 'http://localhost/svn/%s';
|
|||||||
# Path to the monotone binary (you need mtn 0.99 or newer)
|
# Path to the monotone binary (you need mtn 0.99 or newer)
|
||||||
$cfg['mtn_path'] = 'mtn';
|
$cfg['mtn_path'] = 'mtn';
|
||||||
# Additional options for the started monotone process
|
# 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
|
# You can setup monotone for use with indefero in several ways. The
|
||||||
# two most-used should be:
|
# two most-used should be:
|
||||||
@ -157,6 +157,19 @@ $cfg['mtn_remote_url'] = 'mtn://my-host.biz/%s';
|
|||||||
#
|
#
|
||||||
$cfg['mtn_db_access'] = 'remote';
|
$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
|
# If configured, this allows basic control of a running usher process
|
||||||
# via the forge administration. The variable must point to the full (writable)
|
# via the forge administration. The variable must point to the full (writable)
|
||||||
# path of the usher configuration file which gets updated when new projects
|
# path of the usher configuration file which gets updated when new projects
|
||||||
|
@ -88,6 +88,12 @@ Pluf_Signal::connect('gitpostupdate.php::run',
|
|||||||
# monotone synchronization
|
# monotone synchronization
|
||||||
Pluf_Signal::connect('IDF_Project::created',
|
Pluf_Signal::connect('IDF_Project::created',
|
||||||
array('IDF_Plugin_SyncMonotone', 'entry'));
|
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',
|
Pluf_Signal::connect('phppostpush.php::run',
|
||||||
array('IDF_Plugin_SyncMonotone', 'entry'));
|
array('IDF_Plugin_SyncMonotone', 'entry'));
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user