diff --git a/config/Migrations/20231012000007_SGExtend.php b/config/Migrations/20231012000007_SGExtend.php
new file mode 100644
index 0000000..b49bdba
--- /dev/null
+++ b/config/Migrations/20231012000007_SGExtend.php
@@ -0,0 +1,34 @@
+table('sgo');
+ $exists = $table->hasColumn('extend');
+ if (!$exists) {
+ $table
+ ->addColumn('extend', 'boolean', [
+ 'default' => 0,
+ 'null' => false,
+ ])->update();
+ }
+ }
+}
diff --git a/src/Controller/SharingGroupsController.php b/src/Controller/SharingGroupsController.php
index 8afd6c6..56f9a52 100644
--- a/src/Controller/SharingGroupsController.php
+++ b/src/Controller/SharingGroupsController.php
@@ -8,6 +8,7 @@ use Cake\Utility\Text;
use \Cake\Database\Expression\QueryExpression;
use Cake\Error\Debugger;
use Cake\Http\Exception\NotFoundException;
+use Cake\ORM\TableRegistry;
class SharingGroupsController extends AppController
{
@@ -171,9 +172,13 @@ class SharingGroupsController extends AppController
$input['organisation_id'] = [$input['organisation_id']];
}
$result = true;
+ $this->SGO = TableRegistry::getTableLocator()->get('SGOs');
foreach ($input['organisation_id'] as $org_id) {
- $org = $this->SharingGroups->SharingGroupOrgs->get($org_id);
- $result &= (bool)$this->SharingGroups->SharingGroupOrgs->link($sharingGroup, [$org]);
+ $additional_data = [];
+ if (!empty($input['extend'])) {
+ $additional_data['extend'] = $input['extend'];
+ }
+ $result &= $this->SGO->attach($sharingGroup['id'], $org_id, $additional_data);
}
if ($result) {
$message = __('Organisation(s) added to the sharing group.');
@@ -216,8 +221,8 @@ class SharingGroupsController extends AppController
throw new NotFoundException(__('Invalid SharingGroup.'));
}
if ($this->request->is('post')) {
- $org = $this->SharingGroups->SharingGroupOrgs->get($org_id);
- $result = (bool)$this->SharingGroups->SharingGroupOrgs->unlink($sharingGroup, [$org]);
+ $this->SGO = TableRegistry::getTableLocator()->get('SGOs');
+ $result = (bool)$this->SharingGroups->SharingGroupOrgs->unlink($sharingGroup['id'], $org_id);
if ($result) {
$message = __('Organisation(s) removed from the sharing group.');
} else {
@@ -253,9 +258,10 @@ class SharingGroupsController extends AppController
public function listOrgs($id)
{
- $sharingGroup = $this->SharingGroups->get($id, [
- 'contain' => 'SharingGroupOrgs'
- ]);
+ $sharingGroup = $this->SharingGroups->find()->where(['id' => $id])->contain(['SharingGroupOrgs'])->first();
+ foreach ($sharingGroup['sharing_group_orgs'] as $k => $org) {
+ $sharingGroup['sharing_group_orgs'][$k]['extend'] = $org['_joinData']['extend'];
+ }
$params = $this->ParamHandler->harvestParams(['quickFilter']);
if (!empty($params['quickFilter'])) {
foreach ($sharingGroup['sharing_group_orgs'] as $k => $org) {
diff --git a/src/Lib/default/local_tool_connectors/CommonConnectorTools.php b/src/Lib/default/local_tool_connectors/CommonConnectorTools.php
index cacd34c..6bc7f7b 100644
--- a/src/Lib/default/local_tool_connectors/CommonConnectorTools.php
+++ b/src/Lib/default/local_tool_connectors/CommonConnectorTools.php
@@ -104,6 +104,16 @@ class CommonConnectorTools
return $orgs;
}
+ public function getSharingGroups(): array
+ {
+ $sgs = \Cake\ORM\TableRegistry::getTableLocator()->get('SharingGroups');
+ $sgs = $sgs->find()
+ ->contain(['Organisations' => ['fields' => ['uuid']], 'SharingGroupOrgs' => ['fields' => ['uuid']]])
+ ->disableHydration()
+ ->toArray();
+ return $sgs;
+ }
+
public function getFilteredOrganisations($filters, $returnObjects = false): array
{
$organisations = \Cake\ORM\TableRegistry::getTableLocator()->get('Organisations');
@@ -219,6 +229,11 @@ class CommonConnectorTools
$this->remoteToolConnectionStatus($params, self::STATE_CONNECTED);
return false;
}
+
+ public function diagnostics(array $params): array
+ {
+ return [];
+ }
}
?>
diff --git a/src/Lib/default/local_tool_connectors/MispConnector.php b/src/Lib/default/local_tool_connectors/MispConnector.php
index 94f89e9..e6ac7c5 100644
--- a/src/Lib/default/local_tool_connectors/MispConnector.php
+++ b/src/Lib/default/local_tool_connectors/MispConnector.php
@@ -15,6 +15,12 @@ class MispConnector extends CommonConnectorTools
public $name = 'MISP';
public $exposedFunctions = [
+ 'diagnosticsAction' => [
+ 'type' => 'index',
+ 'scope' => 'child',
+ 'params' => [
+ ]
+ ],
'serverSettingsAction' => [
'type' => 'index',
'scope' => 'child',
@@ -48,6 +54,11 @@ class MispConnector extends CommonConnectorTools
'direction'
]
],
+ 'restartWorkersAction' => [
+ 'type' => 'formAction',
+ 'scope' => 'childAction',
+ 'redirect' => 'diagnosticsAction'
+ ],
'fetchOrganisationAction' => [
'type' => 'formAction',
'scope' => 'childAction',
@@ -132,7 +143,7 @@ class MispConnector extends CommonConnectorTools
'icon' => 'terminal',
'variant' => 'primary',
]
- ]
+ ],
];
public $version = '0.1';
public $settings = [
@@ -261,7 +272,7 @@ class MispConnector extends CommonConnectorTools
if (!empty($params['softError'])) {
return $response;
}
- $errorMsg = __('Could not post to the requested resource for `{0}`. Remote returned:', $url) . PHP_EOL . $response->getStringBody();
+ $errorMsg = __('Could not GET from the requested resource for `{0}`. Remote returned:', $url) . PHP_EOL . $response->getStringBody();
$this->logError($errorMsg);
throw new NotFoundException($errorMsg);
}
@@ -312,10 +323,183 @@ class MispConnector extends CommonConnectorTools
return $url;
}
+ public function diagnostics(array $params): array
+ {
+ $urlParams = h($params['connection']['id']) . '/serverSettingsAction';
+ $response = $this->getData('/servers/serverSettings', $params);
+ if (!$response->isOk()) {
+ return [];
+ }
+ $data = $response->getJson();
+ $issues = [];
+ if ($data['version']['upToDate'] !== 'same') {
+ $issues['version'] = [
+ 'type' => 'danger',
+ 'message' => __('Outdated ({0}).', $data['version']['current']),
+ 'remediation' => [
+ 'icon' => 'fa-sync',
+ 'title' => __('Update MISP'),
+ 'url' => '/localTools/action/' . h($params['connection']['id']) . '/updateMISP'
+ ]
+ ];
+ }
+ if ($data['phpSettings']['memory_limit']['value'] < $data['phpSettings']['memory_limit']['recommended']) {
+ $issues['php_memory'] = [
+ 'type' => 'warning',
+ 'message' => __('Low PHP memory ({0}M).', $data['phpSettings']['memory_limit']['value'])
+ ];
+ }
+ $worker_issues = [];
+ foreach ($data['workers'] as $queue => $worker_data) {
+ if (in_array($queue, ['proc_accessible', 'scheduler', 'controls'])) {
+ continue;
+ }
+ if (empty($worker_data['ok'])) {
+ $worker_issues['down'][] = $queue;
+ }
+ if ($worker_data['jobCount'] > 100) {
+ $worker_issues['stalled'][] = $queue;
+ }
+ }
+ if (!empty($worker_issues['down'])) {
+ $issues['workers_down'] = [
+ 'type' => 'danger',
+ 'message' => __('Worker(s) down: {0}', implode(', ', $worker_issues['down'])),
+ 'remediation' => [
+ 'icon' => 'fa-sync',
+ 'title' => __('Restart workers'),
+ 'url' => '/localTools/action/' . h($params['connection']['id']) . '/restartWorkersAction'
+ ]
+ ];
+ }
+
+ if (!empty($worker_issues['stalled'])) {
+ $issues['workers_stalled'] = [
+ 'type' => 'warning',
+ 'message' => __('Worker(s) stalled: {0}', implode(', ', $worker_issues['stalled'])),
+ 'remediation' => [
+ 'icon' => 'fa-sync',
+ 'title' => __('Restart workers'),
+ 'url' => '/localTools/action/' . h($params['connection']['id']) . '/restartWorkersAction'
+ ]
+ ];
+ }
+ if (!empty($data['dbConfiguration'])) {
+ foreach ($data['dbConfiguration'] as $dbConfig) {
+ if ($dbConfig['name'] === 'innodb_buffer_pool_size' && $dbConfig['value'] < $dbConfig['recommended']) {
+ $issues['innodb_buffer_pool_size'] = [
+ 'type' => 'warning',
+ 'message' => __('InnoDB buffer pool size is low ({0}M).', (round($dbConfig['value']/1024/1024)))
+ ];
+ }
+ }
+ }
+ if (!empty($data['dbSchemaDiagnostics'])) {
+ if ($data['dbSchemaDiagnostics']['expected_db_version'] > $data['dbSchemaDiagnostics']['actual_db_version'])
+ $issues['schema_version'] = [
+ 'type' => 'danger',
+ 'message' => __('DB schame outdated.'),
+ 'remediation' => [
+ 'icon' => 'fa-sync',
+ 'title' => __('Update DB schema'),
+ 'url' => '/localTools/action/' . h($params['connection']['id']) . '/updateSchemaAction'
+ ]
+ ];
+ }
+ return $issues;
+ }
+
+ public function restartWorkersAction(array $params): array
+ {
+ if ($params['request']->is(['get'])) {
+ return [
+ 'data' => [
+ 'title' => __('Restart workers'),
+ 'description' => __('Would you like to trigger a restart of all attached workers?'),
+ 'submit' => [
+ 'action' => $params['request']->getParam('action')
+ ],
+ 'url' => ['controller' => 'localTools', 'action' => 'action', $params['connection']['id'], 'restartWorkersAction']
+ ]
+ ];
+ } elseif ($params['request']->is(['post'])) {
+ $response = $this->postData('/servers/restartWorkers', $params);
+ if ($response->isOk()) {
+ return ['success' => 1, 'message' => __('Workers restarted.')];
+ } else {
+ return ['success' => 0, 'message' => __('Could not restart workers.')];
+ }
+ }
+
+
+
+ $response = $this->postData('/servers/restartWorkers', $params);
+ if ($response->isOk()) {
+ return [
+ 'type' => 'success',
+ 'message' => __('Workers restarted.')
+ ];
+ } else {
+ return [
+ 'type' => 'danger',
+ 'message' => __('Something went wrong.')
+ ];
+ }
+ }
+
public function diagnosticsAction(array $params): array
{
-
+ $diagnostics = $this->diagnostics($params);
+ $data = [];
+ foreach ($diagnostics as $error => $error_data) {
+ $data[] = [
+ 'error' => $error,
+ 'type' => $error_data['type'],
+ 'message' => $error_data['message'],
+ 'remediation' => $error_data['remediation'] ?? false
+ ];
+ }
+ return [
+ 'type' => 'index',
+ 'data' => [
+ 'data' => $data,
+ 'skip_pagination' => 1,
+ 'top_bar' => [
+ 'children' => []
+ ],
+ 'fields' => [
+ [
+ 'name' => 'error',
+ 'data_path' => 'error',
+ 'name' => __('Error'),
+ ],
+ [
+ 'name' => 'message',
+ 'data_path' => 'message',
+ 'name' => __('Message'),
+ ],
+ [
+ 'name' => __('Remediation'),
+ 'element' => 'function',
+ 'function' => function($row, $context) {
+ $remediation = $context->Hash->extract($row, 'remediation');
+ if (!empty($remediation['title'])) {
+ echo sprintf(
+ '',
+ h($remediation['url']),
+ h($remediation['title']),
+ h($remediation['icon'])
+ );
+ }
+ }
+ ]
+ ],
+ 'title' => false,
+ 'description' => false,
+ 'pull' => 'right'
+ ]
+ ];
}
public function serverSettingsAction(array $params): array
@@ -576,6 +760,56 @@ class MispConnector extends CommonConnectorTools
}
}
+ private function __compareOrgs(array $data, array $existingOrgs): array
+ {
+ foreach ($data as $k => $v) {
+ $data[$k]['Organisation']['local_copy'] = false;
+ if (!empty($existingOrgs[$v['Organisation']['uuid']])) {
+ $remoteOrg = $existingOrgs[$v['Organisation']['uuid']];
+ $localOrg = $v['Organisation'];
+ $same = true;
+ $fieldsToCheck = [
+ 'nationality', 'sector', 'type', 'name'
+ ];
+ foreach (['nationality', 'sector', 'type', 'name'] as $fieldToCheck) {
+ if ($remoteOrg[$fieldToCheck] != $localOrg[$fieldToCheck]) {
+ $same = false;
+ }
+ }
+ $data[$k]['Organisation']['local_copy'] = $same ? 'same' : 'different';
+ } else {
+ $data[$k]['Organisation']['local_copy'] = 'not_found';
+ }
+ }
+ return $data;
+ }
+
+ private function __compareSgs(array $data, array $existingSgs): array
+ {
+ debug($data);
+ debug($existingSgs);
+ foreach ($data as $k => $v) {
+ $data[$k]['SharingGroup']['local_copy'] = false;
+ if (!empty($existingOrgs[$v['SharingGroup']['uuid']])) {
+ $remoteOrg = $existingOrgs[$v['SharingGroup']['uuid']];
+ $localOrg = $v['SharingGroup'];
+ $same = true;
+ $fieldsToCheck = [
+ 'nationality', 'sector', 'type', 'name'
+ ];
+ foreach (['nationality', 'sector', 'type', 'name'] as $fieldToCheck) {
+ if ($remoteOrg[$fieldToCheck] != $localOrg[$fieldToCheck]) {
+ $same = false;
+ }
+ }
+ $data[$k]['SharingGroup']['local_copy'] = $same ? 'same' : 'different';
+ } else {
+ $data[$k]['SharingGroup']['local_copy'] = 'not_found';
+ }
+ }
+ return $data;
+ }
+
public function organisationsAction(array $params): array
{
@@ -610,25 +844,7 @@ class MispConnector extends CommonConnectorTools
'icon' => 'exclamation-triangle'
]
];
- foreach ($data as $k => $v) {
- $data[$k]['Organisation']['local_copy'] = false;
- if (!empty($existingOrgs[$v['Organisation']['uuid']])) {
- $remoteOrg = $existingOrgs[$v['Organisation']['uuid']];
- $localOrg = $v['Organisation'];
- $same = true;
- $fieldsToCheck = [
- 'nationality', 'sector', 'type', 'name'
- ];
- foreach (['nationality', 'sector', 'type', 'name'] as $fieldToCheck) {
- if ($remoteOrg[$fieldToCheck] != $localOrg[$fieldToCheck]) {
- $same = false;
- }
- }
- $data[$k]['Organisation']['local_copy'] = $same ? 'same' : 'different';
- } else {
- $data[$k]['Organisation']['local_copy'] = 'not_found';
- }
- }
+ $data = $this->__compareOrgs($data, $existingOrgs);
if (!empty($data)) {
return [
'type' => 'index',
@@ -764,6 +980,35 @@ class MispConnector extends CommonConnectorTools
$urlParams = h($params['connection']['id']) . '/sharingGroupsAction';
$response = $this->getData('/sharing_groups/index', $params);
$data = $response->getJson();
+ $temp = $this->getSharingGroups();
+ $existingOrgs = [];
+ foreach ($temp as $k => $v) {
+ $existingSGs[$v['uuid']] = $v;
+ unset($temp[$k]);
+ }
+ $data = $this->__compareSgs($data, $existingSGs);
+ $existingSGs = [];
+ foreach ($temp as $k => $v) {
+ $existingSGs[$v['uuid']] = $v;
+ unset($temp[$k]);
+ }
+ $statusLevels = [
+ 'same' => [
+ 'colour' => 'success',
+ 'message' => __('Remote sharing group is the same as local copy'),
+ 'icon' => 'check-circle'
+ ],
+ 'different' => [
+ 'colour' => 'warning',
+ 'message' => __('Local and remote versions of the sharing groups are different.'),
+ 'icon' => 'exclamation-circle'
+ ],
+ 'not_found' => [
+ 'colour' => 'danger',
+ 'message' => __('Local sharing group not found'),
+ 'icon' => 'exclamation-triangle'
+ ]
+ ];
if (!empty($data)) {
return [
'type' => 'index',
@@ -773,21 +1018,48 @@ class MispConnector extends CommonConnectorTools
'top_bar' => [
'children' => [
[
- 'type' => 'search',
- 'button' => __('Search'),
- 'placeholder' => __('Enter value to search'),
- 'data' => '',
- 'searchKey' => 'value',
- 'additionalUrlParams' => $urlParams
- ]
+ 'type' => 'simple',
+ 'children' => [
+ [
+ 'class' => 'hidden mass-select',
+ 'text' => __('Fetch selected sharing groups'),
+ 'html' => ' ',
+ 'reload_url' => '/localTools/action/' . h($params['connection']['id']) . '/sharingGroupsAction',
+ 'popover_url' => '/localTools/action/' . h($params['connection']['id']) . '/fetchSelectedSharingGroupsAction'
+ ],
+ [
+ 'text' => __('Push sharing groups'),
+ 'html' => ' ',
+ 'class' => 'btn btn-primary',
+ 'reload_url' => '/localTools/action/' . h($params['connection']['id']) . '/SharingGroupsAction',
+ 'popover_url' => '/localTools/action/' . h($params['connection']['id']) . '/pushSharingGroupsAction'
+ ]
+ ]
+ ],
]
],
'fields' => [
+ [
+ 'element' => 'selector',
+ 'class' => 'short',
+ 'data' => [
+ 'id' => [
+ 'value_path' => 'SharingGroup.uuid'
+ ]
+ ]
+ ],
[
'name' => 'Name',
'sort' => 'SharingGroup.name',
'data_path' => 'SharingGroup.name',
],
+ [
+ 'name' => 'Status',
+ 'sort' => 'SharingGroup.local_copy',
+ 'data_path' => 'SharingGroup.local_copy',
+ 'element' => 'status',
+ 'status_levels' => $statusLevels
+ ],
[
'name' => 'uuid',
'sort' => 'SharingGroup.uuid',
diff --git a/src/Model/Entity/SGO.php b/src/Model/Entity/SGO.php
new file mode 100644
index 0000000..39d900b
--- /dev/null
+++ b/src/Model/Entity/SGO.php
@@ -0,0 +1,11 @@
+ $broods,
- 'tools' => $LocalToolsModel->extractMeta($connectors, true)
+ 'tools' => $connections
];
if ($mermaid) {
return $this->generateTopologyMermaid($data);
@@ -319,8 +319,19 @@ class InstanceTable extends AppTable
h($tool['name'])
);
foreach ($tool['connections'] as $k2 => $connection) {
+ $diagnostic_output = '';
+ if (!empty($connection['diagnostics'])) {
+ foreach ($connection['diagnostics'] as $diagnostic => $diagnostic_data) {
+ $diagnostic_output .= sprintf(
+ "%s: %s
",
+ h($diagnostic),
+ h($diagnostic_data['type']),
+ h($diagnostic_data['message'])
+ );
+ }
+ }
$tools .= sprintf(
- " connection%s[%s
%s
%s]" . PHP_EOL,
+ " connection%s[\"%s
%s
%s%s\"]" . PHP_EOL,
h($k2),
h($connection['name']),
sprintf(
@@ -329,6 +340,7 @@ class InstanceTable extends AppTable
$connection['health'] === 1 ? 'text-success' : 'text-danger',
$connection['health'] === 1 ? 'fas:fa-check' : 'fas:fa-times'
),
+ empty($diagnostic_data) ? '' : 'Diagnostics:
' . $diagnostic_output,
sprintf(
"fas:fa-eye",
h($connection['url'])
diff --git a/src/Model/Table/LocalToolsTable.php b/src/Model/Table/LocalToolsTable.php
index f764ace..3d66c62 100644
--- a/src/Model/Table/LocalToolsTable.php
+++ b/src/Model/Table/LocalToolsTable.php
@@ -147,14 +147,14 @@ class LocalToolsTable extends AppTable
'connector_settings_placeholder' => $connector_class->settingsPlaceholder ?? [],
];
if ($includeConnections) {
- $connector['connections'] = $this->healthCheck($connector_type, $connector_class);
+ $connector['connections'] = $this->healthCheck($connector_type, $connector_class, true);
}
$connectors[] = $connector;
}
return $connectors;
}
- public function healthCheck(string $connector_type, Object $connector_class): array
+ public function healthCheck(string $connector_type, Object $connector_class, bool $includeDiagnostics = false): array
{
$query = $this->find();
$query->where([
@@ -162,11 +162,28 @@ class LocalToolsTable extends AppTable
]);
$connections = $query->all()->toList();
foreach ($connections as &$connection) {
- $connection = $this->healthCheckIndividual($connection);
+ $temp = $this->healthCheckIndividual($connection);
+ if ($includeDiagnostics && !empty($temp['health']) && $temp['health'] === 1) {
+ $temp['diagnostics'] = $this->diagnosticCheckIndividual($connection);
+ }
+ $connection = $temp;
}
return $connections;
}
+ public function diagnosticCheckIndividual(Object $connection): array
+ {
+ $connector_class = $this->getConnectors($connection->connector);
+ if (empty($connector_class[$connection->connector])) {
+ return [];
+ }
+ $connector_class = $connector_class[$connection->connector];
+ return $connector_class->diagnostics([
+ 'connection' => $connection,
+ 'softError' => 1
+ ]);
+ }
+
public function healthCheckIndividual(Object $connection): array
{
$connector_class = $this->getConnectors($connection->connector);
diff --git a/src/Model/Table/SGOsTable.php b/src/Model/Table/SGOsTable.php
new file mode 100644
index 0000000..e8d05ee
--- /dev/null
+++ b/src/Model/Table/SGOsTable.php
@@ -0,0 +1,53 @@
+setTable('sgo');
+ parent::initialize($config);
+ $this->belongsTo('SharingGroups');
+ $this->belongsTo('Organisations');
+ }
+
+ public function attach(int $sg_id, int $org_id, array $additional_data = []): bool
+ {
+ $sgo = $this->find()->where([
+ 'sharing_group_id' => $sg_id,
+ 'organisation_id' => $org_id
+ ])->first();
+ if (empty($sgo)) {
+ $sgo = $this->newEmptyEntity();
+ $sgo->sharing_group_id = $sg_id;
+ $sgo->organisation_id = $org_id;
+ }
+ $sgo->extend = empty($additional_data['extend']) ? 0 : 1;
+ if ($this->save($sgo)) {
+ return true;
+ }
+ return false;
+
+ }
+
+ public function detach(): bool
+ {
+ $sgo = $this->find()->where([
+ 'sharing_group_id' => $sg_id,
+ 'organisation_id' => $org_id
+ ])->first();
+ if (!empty($sgo)) {
+ if (!$this->delete($sgo)) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/src/Model/Table/SharingGroupsTable.php b/src/Model/Table/SharingGroupsTable.php
index e976e8c..25c7efd 100644
--- a/src/Model/Table/SharingGroupsTable.php
+++ b/src/Model/Table/SharingGroupsTable.php
@@ -25,11 +25,11 @@ class SharingGroupsTable extends AppTable
$this->belongsToMany(
'SharingGroupOrgs',
[
+ 'through' => 'SGOs',
'className' => 'Organisations',
'foreignKey' => 'sharing_group_id',
- 'joinTable' => 'sgo',
'targetForeignKey' => 'organisation_id'
- ]
+ ],
);
$this->setDisplayField('name');
}
@@ -77,11 +77,14 @@ class SharingGroupsTable extends AppTable
public function postCaptureActions($savedEntity, $input): void
{
- $orgs = [];
+ $additional_data = [];
+ if (!empty($input['extend'])) {
+ $additional_data['extend'] = $input['extend'];
+ }
+ $this->SGO = TableRegistry::getTableLocator()->get('SGOs');
foreach ($input['sharing_group_orgs'] as $sgo) {
$organisation_id = $this->Organisations->captureOrg($sgo);
- $orgs[] = $this->SharingGroupOrgs->get($organisation_id);
+ $this->SGO->attach($savedEntity->id, $organisation_id, $additional_data);
}
- $this->SharingGroupOrgs->link($savedEntity, $orgs);
}
}
diff --git a/templates/SharingGroups/add_org.php b/templates/SharingGroups/add_org.php
index 79ce801..f5e949c 100644
--- a/templates/SharingGroups/add_org.php
+++ b/templates/SharingGroups/add_org.php
@@ -9,6 +9,11 @@
'label' => __('Owner organisation'),
'options' => $dropdownData['organisation']
],
+ [
+ 'field' => 'extend',
+ 'type' => 'checkbox',
+ 'label' => __('Can extend/administer')
+ ],
],
'submit' => [
'action' => $this->request->getParam('action')
diff --git a/templates/SharingGroups/list_orgs.php b/templates/SharingGroups/list_orgs.php
index 267677f..77918ec 100644
--- a/templates/SharingGroups/list_orgs.php
+++ b/templates/SharingGroups/list_orgs.php
@@ -42,6 +42,13 @@ echo $this->element('genericElements/IndexTable/index_table', [
'sort' => 'uuid',
'class' => 'short',
'data_path' => 'uuid',
+ ],
+ [
+ 'name' => __('Can extend/administer'),
+ 'sort' => 'extend',
+ 'element' => 'boolean',
+ 'class' => 'short',
+ 'data_path' => 'extend',
]
],
'pull' => 'right',