Added org merge tool

- allows a site admin to merge all objects belonging to an organisation into another
  - this can be useful if duplicate organisations exist for example
  - the tool overrides the built in mechanism and should only be used if absolutely required
  - at the end of the process the original organisation is removed

- the tool generates 2 files that are dropped in the log directory of MISP
  - 1 contains a JSON with all the changed fields and the IDs
  - 1 contains an SQL script that allows an admin to revert the changes
pull/893/head v2.4.13
Iglocska 2016-01-27 23:59:34 +01:00
parent 430f8ea479
commit 1362cf40b7
8 changed files with 165 additions and 9 deletions

View File

@ -1500,7 +1500,6 @@ class EventsController extends AppController {
if ($to) $to = $this->Event->dateFieldCheck($to);
if ($tags) $tags = str_replace(';', ':', $tags);
if ($last) $last = $this->Event->resolveTimeDelta($last);
$eventIdArray = array();
if ($eventid) {

View File

@ -206,7 +206,35 @@ class OrganisationsController extends AppController {
return new CakeResponse(array('body'=> json_encode($orgs)));
}
public function adminMerge() {
public function admin_merge($id) {
if (!$this->_isSiteAdmin()) throw new MethodNotAllowedException('You are not authorised to do that.');
if ($this->request->is('Post')) {
$result = $this->Organisation->orgMerge($id, $this->request->data, $this->Auth->user());
if ($result) $this->Session->setFlash('The organisation has been successfully merged.');
else $this->Session->setFlash('There was an error while merging the organisations. To find out more about what went wrong, refer to the audit logs. If you would like to revert the changes, you can find a .sql file ');
$this->redirect(array('admin' => false, 'action' => 'index'));
} else {
$currentOrg = $this->Organisation->find('first', array('fields' => array('id', 'name', 'uuid', 'local'), 'recursive' => -1, 'conditions' => array('Organisation.id' => $id)));
$orgs['local'] = $this->Organisation->find('all', array(
'fields' => array('id', 'name', 'uuid'),
'conditions' => array('Organisation.id !=' => $id, 'Organisation.local' => true),
'order' => 'lower(Organisation.name) ASC'
));
$orgs['external'] = $this->Organisation->find('all', array(
'fields' => array('id', 'name', 'uuid'),
'conditions' => array('Organisation.id !=' => $id, 'Organisation.local' => false),
'order' => 'lower(Organisation.name) ASC'
));
foreach (array('local', 'external') as $type) {
$orgOptions[$type] = Hash::combine($orgs[$type], '{n}.Organisation.id', '{n}.Organisation.name');
$orgs[$type] = Hash::combine($orgs[$type], '{n}.Organisation.id', '{n}');
}
$this->set('orgs', json_encode($orgs));
$this->set('orgOptions', $orgOptions);
$this->set('currentOrg', $currentOrg);
$this->layout = false;
$this->autoRender = false;
$this->render('ajax/merge');
}
}
}

View File

@ -41,7 +41,8 @@ class Log extends AppModel {
'reset_auth_key',
'update',
'enable',
'disable'
'disable',
'merge'
)),
'message' => 'Options : ...'
)

View File

@ -60,6 +60,18 @@ class Organisation extends AppModel{
public $countries = array('Not specified', 'International', 'Afghanistan', 'Albania', 'Algeria', 'Andorra', 'Angola', 'Antigua & Deps', 'Argentina', 'Armenia', 'Australia', 'Austria', 'Azerbaijan', 'Bahamas', 'Bahrain', 'Bangladesh', 'Barbados', 'Belarus', 'Belgium', 'Belize', 'Benin', 'Bhutan', 'Bolivia', 'Bosnia Herzegovina', 'Botswana', 'Brazil', 'Brunei', 'Bulgaria', 'Burkina', 'Burundi', 'Cambodia', 'Cameroon', 'Canada', 'Cape Verde', 'Central African Rep', 'Chad', 'Chile', 'China', 'Colombia', 'Comoros', 'Congo', 'Congo {Democratic Rep}', 'Costa Rica', 'Croatia', 'Cuba', 'Cyprus', 'Czech Republic', 'Denmark', 'Djibouti', 'Dominica', 'Dominican Republic', 'East Timor', 'Ecuador', 'Egypt', 'El Salvador', 'Equatorial Guinea', 'Eritrea', 'Estonia', 'Ethiopia', 'Fiji', 'Finland', 'France', 'Gabon', 'Gambia', 'Georgia', 'Germany', 'Ghana', 'Greece', 'Grenada', 'Guatemala', 'Guinea', 'Guinea-Bissau', 'Guyana', 'Haiti', 'Honduras', 'Hungary', 'Iceland', 'India', 'Indonesia', 'Iran', 'Iraq', 'Ireland {Republic}', 'Israel', 'Italy', 'Ivory Coast', 'Jamaica', 'Japan', 'Jordan', 'Kazakhstan', 'Kenya', 'Kiribati', 'Korea North', 'Korea South', 'Kosovo', 'Kuwait', 'Kyrgyzstan', 'Laos', 'Latvia', 'Lebanon', 'Lesotho', 'Liberia', 'Libya', 'Liechtenstein', 'Lithuania', 'Luxembourg', 'Macedonia', 'Madagascar', 'Malawi', 'Malaysia', 'Maldives', 'Mali', 'Malta', 'Marshall Islands', 'Mauritania', 'Mauritius', 'Mexico', 'Micronesia', 'Moldova', 'Monaco', 'Mongolia', 'Montenegro', 'Morocco', 'Mozambique', 'Myanmar, {Burma}', 'Namibia', 'Nauru', 'Nepal', 'Netherlands', 'New Zealand', 'Nicaragua', 'Niger', 'Nigeria', 'Norway', 'Oman', 'Pakistan', 'Palau', 'Panama', 'Papua New Guinea', 'Paraguay', 'Peru', 'Philippines', 'Poland', 'Portugal', 'Qatar', 'Romania', 'Russian Federation', 'Rwanda', 'St Kitts & Nevis', 'St Lucia', 'Saint Vincent & the Grenadines', 'Samoa', 'San Marino', 'Sao Tome & Principe', 'Saudi Arabia', 'Senegal', 'Serbia', 'Seychelles', 'Sierra Leone', 'Singapore', 'Slovakia', 'Slovenia', 'Solomon Islands', 'Somalia', 'South Africa', 'South Sudan', 'Spain', 'Sri Lanka', 'Sudan', 'Suriname', 'Swaziland', 'Sweden', 'Switzerland', 'Syria', 'Taiwan', 'Tajikistan', 'Tanzania', 'Thailand', 'Togo', 'Tonga', 'Trinidad & Tobago', 'Tunisia', 'Turkey', 'Turkmenistan', 'Tuvalu', 'Uganda', 'Ukraine', 'United Arab Emirates', 'United Kingdom', 'United States', 'Uruguay', 'Uzbekistan', 'Vanuatu', 'Vatican City', 'Venezuela', 'Vietnam', 'Yemen', 'Zambia', 'Zimbabwe');
public $organisationAssociations = array(
'Correlation' => array('table' => 'correlations', 'fields' => array('org_id')),
'Event' => array('table' => 'events', 'fields' => array('org_id', 'orgc_id')),
'Job' => array('table' => 'jobs', 'fields' => array('org_id')),
'Server' => array('table' => 'servers', 'fields' => array('org_id', 'remote_org_id')),
'ShadowAttribute' =>array('table' => 'shadow_attributes', 'fields' => array('org_id', 'event_org_id')),
'SharingGroup' => array('table' => 'sharing_groups', 'fields' => array('org_id')),
'SharingGroupOrg' => array('table' => 'sharing_group_orgs', 'fields' => array('org_id')),
'Thread' => array('table' => 'threads', 'fields' => array('org_id')),
'User' => array('table' => 'users', 'fields' => array('org_id'))
);
/*
public $hasAndBelongsToMany = array(
'SharingGroup' => array(
@ -158,4 +170,79 @@ class Organisation extends AppModel{
}
return $existingOrg[$this->alias]['id'];
}
public function orgMerge($id, $request, $user) {
$currentOrg = $this->find('first', array('recursive' => -1, 'conditions' => array('Organisation.id' => $id)));
$targetOrgId = $request['Organisation']['targetType'] == 0 ? $request['Organisation']['orgsLocal'] : $request['Organisation']['orgsExternal'];
$targetOrg = $this->find(
'first', array(
'fields' => array('id', 'name', 'uuid', 'local'),
'recursive' => -1,
'conditions' => array('Organisation.id' => $targetOrgId)
));
if (empty($currentOrg) || empty($targetOrg)) throw new MethodNotAllowedException('Something went wrong with the organisation merge. Organisation not found.');
$dir = new Folder();
$this->Log = ClassRegistry::init('Log');
$dirPath = APP . 'tmp' . DS . 'logs' . DS . 'merges';
if (!$dir->create($dirPath)) throw new MethodNotAllowedException('Merge halted because the log directory (default: /var/www/MISP/app/tmp/logs/merges) could not be created. This is most likely a permission issue, make sure that MISP can write to the logs directory and try again.');
$logFile = new File($dirPath . DS . 'merge_' . $currentOrg['Organisation']['id'] . '_' . $targetOrg['Organisation']['id'] . '_' . time() . '.log');
if (!$logFile->create()) throw new MethodNotAllowedException('Merge halted because the log file (default location: /var/www/MISP/app/tmp/logs/merges/[old_org_id]_[new_org_id]_timestamp.log) could not be created. This is most likely a permission issue, make sure that MISP can write to the logs directory and try again.');
$backupFile = new File($dirPath . DS . 'merge_' . $currentOrg['Organisation']['id'] . '_' . $targetOrg['Organisation']['id'] . '_' . time() . '.sql');
if (!$backupFile->create()) throw new MethodNotAllowedException('Merge halted because the backup script file (default location: /var/www/MISP/app/tmp/logs/merges/[old_org_id]_[new_org_id]_timestamp.sql) could not be created. This is most likely a permission issue, make sure that MISP can write to the logs directory and try again.');
$backupFile->append('INSERT INTO `organisations` (`' . implode('`, `', array_keys($currentOrg['Organisation'])) . '`) VALUES (\'' . implode('\', \'', array_values($currentOrg['Organisation'])) . '\');' . PHP_EOL);
$this->Log->create();
$this->Log->save(array(
'org' => $user['Organisation']['name'],
'model' => 'Organisation',
'model_id' => $currentOrg['Organisation']['id'],
'email' => $user['email'],
'action' => 'merge',
'user_id' => $user['id'],
'title' => 'Starting merger of ' . $currentOrg['Organisation']['name'] . '(' . $currentOrg['Organisation']['id'] . ') into ' . $targetOrg['Organisation']['name'] . '(' . $targetOrg['Organisation']['name'] . ')',
'change' => '',
));
$dataMoved = array('removed_org' => $currentOrg);
$success = true;
foreach ($this->organisationAssociations as $model => $data) {
foreach ($data['fields'] as $field) {
$temp = $this->query('SELECT `id` FROM `' . $data['table'] . '` WHERE `' . $field . '` = "' . $currentOrg['Organisation']['id'] . '"');
if (!empty($temp)) {
$dataMoved['values_changed'][$model][$field] = Set::extract('/' . $data['table'] . '/id', $temp);
if (!empty($dataMoved['values_changed'][$model][$field])) {
$this->Log->create();
try {
$result = $this->query('UPDATE `' . $data['table'] . '` SET `' . $field . '` = ' . $targetOrg['Organisation']['id'] . ' WHERE `' . $field . '` = ' . $currentOrg['Organisation']['id'] . ';');
$backupFile->append('UPDATE `' . $data['table'] . '` SET `' . $field . '` = ' . $currentOrg['Organisation']['id'] . ' WHERE `id` IN (' . implode(',', $dataMoved['values_changed'][$model][$field]) . ');' . PHP_EOL);
$this->Log->save(array(
'org' => $user['Organisation']['name'],
'model' => 'Organisation',
'model_id' => $currentOrg['Organisation']['id'],
'email' => $user['email'],
'action' => 'merge',
'user_id' => $user['id'],
'title' => 'Update for ' . $model . '.' . $field . ' has completed successfully.',
'change' => '',
));
} catch (Exception $e) {
$this->Log->save(array(
'org' => $user['Organisation']['name'],
'model' => 'Organisation',
'model_id' => $currentOrg['Organisation']['id'],
'email' => $user['email'],
'action' => 'merge',
'user_id' => $user['id'],
'title' => 'Update for ' . $model . '.' . $field . ' has failed.',
'change' => json_encode($e->getMessage()),
));
}
}
}
}
}
if ($success) $this->delete($currentOrg['Organisation']['id']);
$backupFile->close();
$logFile->write(json_encode($dataMoved));
$logFile->close();
return $success;
}
}

View File

@ -77,8 +77,8 @@
<div class="tabMenu tabMenuToolsBlock noPrint">
<?php if ($mayModify): ?>
<span id="create-button" title="Populate using a template" class="icon-list-alt useCursorPointer" onClick="getPopup(<?php echo $event['Event']['id']; ?>, 'templates', 'templateChoices');"></span>
<?php endif; ?>
<span id="freetext-button" title="Populate using the freetext import tool" class="icon-exclamation-sign useCursorPointer" onClick="getPopup(<?php echo $event['Event']['id']; ?>, 'events', 'freeTextImport');"></span>
<?php endif; ?>
<span id="freetext-button" title="Populate using the freetext import tool" class="icon-exclamation-sign icon-inverse useCursorPointer" onClick="getPopup(<?php echo $event['Event']['id']; ?>, 'events', 'freeTextImport');"></span>
<?php if ($mayModify): ?>
<span id="attribute-replace-button" title="Replace all attributes of a category/type combination within the event" class="icon-random useCursorPointer" onClick="getPopup(<?php echo $event['Event']['id']; ?>, 'attributes', 'attributeReplace');"></span>
<?php endif; ?>

View File

@ -179,6 +179,7 @@
<li id='liaddOrg'><a href="<?php echo $baseurl;?>/admin/organisations/add">Add Organisation</a></li>
<?php if ($menuItem === 'editOrg' || $menuItem === 'viewOrg'): ?>
<li id='lieditOrg'><a href="<?php echo $baseurl;?>/admin/organisations/edit/<?php echo h($id);?>">Edit Organisation</a></li>
<li id='limergeOrg'><a class="useCursorPointer" onClick="getPopup('<?php echo h($id); ?>', 'organisations', 'merge', 'admin');">Merge Organisation</a></li>
<?php endif;?>
<?php if ($menuItem === 'editOrg' || $menuItem === 'viewOrg'): ?>
<li id='liviewOrg'><a href="<?php echo $baseurl;?>/organisations/view/<?php echo h($id);?>">View Organisation</a></li>

View File

@ -770,13 +770,15 @@ a.proposal_link_red:hover {
z-index:5;
}
.ajax_popover_form legend {
.ajax_popover_form legend, .ajax_popover_form .legend {
border-radius: 10px 10px 0px 0px;
padding-left:10px;
margin-bottom:5px;
margin-bottom:0px;
width:690px;
background-color:black;
color:white;
font-size: 21px;
line-height: 40px;
}
.ajax_popover_form form {
@ -1431,6 +1433,10 @@ a.proposal_link_red:hover {
font-weight: bold;
}
.align-right {
text-align: right;
}
.hidden {
display:none;
}
@ -1450,6 +1456,17 @@ a.discrete {
border-left: 1px solid grey;
}
.bottomGap {
margin-bottom:5px;
}
.highlightedBlock {
border:1px solid black;
background-color:white;
padding:5px;
border-radius:5px;
}
@-webkit-keyframes rotation {
from {-webkit-transform: rotate(0deg);}
to {-webkit-transform: rotate(359deg);}

View File

@ -893,9 +893,10 @@ function templateElementFileCategoryChange(category) {
}
}
function getPopup(id, context, target) {
function getPopup(id, context, target, admin) {
$("#gray_out").fadeIn();
var url = "";
if (typeof admin !== 'undefined') url+= "/admin";
if (context != '') url += "/" + context;
if (target != '') url += "/" + target;
if (id != '') url += "/" + id;
@ -2164,3 +2165,25 @@ function filterAttributes(filter, id) {
}
});
}
function mergeOrganisationUpdate() {
var orgTypeOptions = ['local', 'external'];
var orgTypeSelects = ['OrganisationOrgsLocal', 'OrganisationOrgsExternal'];
orgType = orgTypeSelects[$('#OrganisationTargetType').val()];
orgID = $('#' + orgType).val();
org = orgArray[orgTypeOptions[$('#OrganisationTargetType').val()]][orgID]['Organisation'];
$('#org_id').text(org['id']);
$('#org_name').text(org['name']);
$('#org_uuid').text(org['uuid']);
$('#org_local').text(orgTypeOptions[$('#OrganisationTargetType').val()]);
}
function mergeOrganisationTypeToggle() {
if ($('#OrganisationTargetType').val() == 0) {
$('#orgsLocal').show();
$('#orgsExternal').hide();
} else {
$('#orgsLocal').hide();
$('#orgsExternal').show();
}
}