@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Allow Almanac bindings to be disabled and unused interfaces to be removed

Summary:
Fixes T9762. Ref T10246.

**Disabling Bindings**: Previously, there was no formal way to disable bindings. The internal callers sometimes check some informal property on the binding, but this is a common need and deserves first-class support in the UI. Allow bindings to be disabled.

**Deleting Interfaces**: Previously, you could not delete interfaces. Now, you can delete unused interfaces.

Also some minor cleanup and slightly less mysterious documentation.

Test Plan: Disabled bindings and deleted interfaces.

Reviewers: chad

Reviewed By: chad

Subscribers: yelirekim

Maniphest Tasks: T9762, T10246

Differential Revision: https://secure.phabricator.com/D15345

+357 -14
+2
resources/sql/autopatches/20160225.almanac.1.disablebinding.sql
··· 1 + ALTER TABLE {$NAMESPACE}_almanac.almanac_binding 2 + ADD isDisabled BOOL NOT NULL;
+5
src/__phutil_library_map__.php
··· 11 11 'class' => array( 12 12 'AlmanacAddress' => 'applications/almanac/util/AlmanacAddress.php', 13 13 'AlmanacBinding' => 'applications/almanac/storage/AlmanacBinding.php', 14 + 'AlmanacBindingDisableController' => 'applications/almanac/controller/AlmanacBindingDisableController.php', 14 15 'AlmanacBindingEditController' => 'applications/almanac/controller/AlmanacBindingEditController.php', 15 16 'AlmanacBindingEditor' => 'applications/almanac/editor/AlmanacBindingEditor.php', 16 17 'AlmanacBindingPHIDType' => 'applications/almanac/phid/AlmanacBindingPHIDType.php', ··· 51 52 'AlmanacEditor' => 'applications/almanac/editor/AlmanacEditor.php', 52 53 'AlmanacInterface' => 'applications/almanac/storage/AlmanacInterface.php', 53 54 'AlmanacInterfaceDatasource' => 'applications/almanac/typeahead/AlmanacInterfaceDatasource.php', 55 + 'AlmanacInterfaceDeleteController' => 'applications/almanac/controller/AlmanacInterfaceDeleteController.php', 54 56 'AlmanacInterfaceEditController' => 'applications/almanac/controller/AlmanacInterfaceEditController.php', 55 57 'AlmanacInterfacePHIDType' => 'applications/almanac/phid/AlmanacInterfacePHIDType.php', 56 58 'AlmanacInterfaceQuery' => 'applications/almanac/query/AlmanacInterfaceQuery.php', ··· 3996 3998 'PhabricatorDestructibleInterface', 3997 3999 'PhabricatorExtendedPolicyInterface', 3998 4000 ), 4001 + 'AlmanacBindingDisableController' => 'AlmanacServiceController', 3999 4002 'AlmanacBindingEditController' => 'AlmanacServiceController', 4000 4003 'AlmanacBindingEditor' => 'AlmanacEditor', 4001 4004 'AlmanacBindingPHIDType' => 'PhabricatorPHIDType', ··· 4049 4052 'AlmanacDAO', 4050 4053 'PhabricatorPolicyInterface', 4051 4054 'PhabricatorDestructibleInterface', 4055 + 'PhabricatorExtendedPolicyInterface', 4052 4056 ), 4053 4057 'AlmanacInterfaceDatasource' => 'PhabricatorTypeaheadDatasource', 4058 + 'AlmanacInterfaceDeleteController' => 'AlmanacDeviceController', 4054 4059 'AlmanacInterfaceEditController' => 'AlmanacDeviceController', 4055 4060 'AlmanacInterfacePHIDType' => 'PhabricatorPHIDType', 4056 4061 'AlmanacInterfaceQuery' => 'AlmanacQuery',
+15 -1
src/applications/almanac/application/PhabricatorAlmanacApplication.php
··· 55 55 ), 56 56 'interface/' => array( 57 57 'edit/(?:(?P<id>\d+)/)?' => 'AlmanacInterfaceEditController', 58 + 'delete/(?:(?P<id>\d+)/)?' => 'AlmanacInterfaceDeleteController', 58 59 ), 59 60 'binding/' => array( 60 61 'edit/(?:(?P<id>\d+)/)?' => 'AlmanacBindingEditController', 62 + 'disable/(?:(?P<id>\d+)/)?' => 'AlmanacBindingDisableController', 61 63 '(?P<id>\d+)/' => 'AlmanacBindingViewController', 62 64 ), 63 65 'network/' => array( ··· 80 82 } 81 83 82 84 protected function getCustomCapabilities() { 85 + $cluster_caption = pht( 86 + 'This permission is very dangerous. %s', 87 + phutil_tag( 88 + 'a', 89 + array( 90 + 'href' => PhabricatorEnv::getDoclink( 91 + 'User Guide: Phabricator Clusters'), 92 + 'target' => '_blank', 93 + ), 94 + pht('Learn More'))); 95 + 83 96 return array( 84 97 AlmanacCreateServicesCapability::CAPABILITY => array( 85 98 'default' => PhabricatorPolicies::POLICY_ADMIN, ··· 94 107 'default' => PhabricatorPolicies::POLICY_ADMIN, 95 108 ), 96 109 AlmanacManageClusterServicesCapability::CAPABILITY => array( 97 - 'default' => PhabricatorPolicies::POLICY_ADMIN, 110 + 'default' => PhabricatorPolicies::POLICY_NOONE, 111 + 'caption' => $cluster_caption, 98 112 ), 99 113 ); 100 114 }
+69
src/applications/almanac/controller/AlmanacBindingDisableController.php
··· 1 + <?php 2 + 3 + final class AlmanacBindingDisableController 4 + extends AlmanacServiceController { 5 + 6 + public function handleRequest(AphrontRequest $request) { 7 + $viewer = $request->getViewer(); 8 + 9 + $id = $request->getURIData('id'); 10 + $binding = id(new AlmanacBindingQuery()) 11 + ->setViewer($viewer) 12 + ->withIDs(array($id)) 13 + ->requireCapabilities( 14 + array( 15 + PhabricatorPolicyCapability::CAN_VIEW, 16 + PhabricatorPolicyCapability::CAN_EDIT, 17 + )) 18 + ->executeOne(); 19 + if (!$binding) { 20 + return new Aphront404Response(); 21 + } 22 + 23 + $id = $binding->getID(); 24 + $is_disable = !$binding->getIsDisabled(); 25 + $done_uri = $binding->getURI(); 26 + 27 + if ($is_disable) { 28 + $disable_title = pht('Disable Binding'); 29 + $disable_body = pht('Disable this binding?'); 30 + $disable_button = pht('Disable Binding'); 31 + 32 + $v_disable = 1; 33 + } else { 34 + $disable_title = pht('Enable Binding'); 35 + $disable_body = pht('Enable this binding?'); 36 + $disable_button = pht('Enable Binding'); 37 + 38 + $v_disable = 0; 39 + } 40 + 41 + 42 + if ($request->isFormPost()) { 43 + $type_disable = AlmanacBindingTransaction::TYPE_DISABLE; 44 + 45 + $xactions = array(); 46 + 47 + $xactions[] = id(new AlmanacBindingTransaction()) 48 + ->setTransactionType($type_disable) 49 + ->setNewValue($v_disable); 50 + 51 + $editor = id(new AlmanacBindingEditor()) 52 + ->setActor($viewer) 53 + ->setContentSourceFromRequest($request) 54 + ->setContinueOnNoEffect(true) 55 + ->setContinueOnMissingFields(true); 56 + 57 + $editor->applyTransactions($binding, $xactions); 58 + 59 + return id(new AphrontRedirectResponse())->setURI($done_uri); 60 + } 61 + 62 + return $this->newDialog() 63 + ->setTitle($disable_title) 64 + ->appendParagraph($disable_body) 65 + ->addSubmitButton($disable_button) 66 + ->addCancelButton($done_uri); 67 + } 68 + 69 + }
+22
src/applications/almanac/controller/AlmanacBindingViewController.php
··· 35 35 ->setHeader($title) 36 36 ->setPolicyObject($binding); 37 37 38 + if ($binding->getIsDisabled()) { 39 + $header->setStatus('fa-ban', 'red', pht('Disabled')); 40 + } 41 + 38 42 $box = id(new PHUIObjectBoxView()) 39 43 ->setHeader($header) 40 44 ->addPropertyList($property_list); ··· 112 116 ->setName(pht('Edit Binding')) 113 117 ->setHref($this->getApplicationURI("binding/edit/{$id}/")) 114 118 ->setWorkflow(!$can_edit) 119 + ->setDisabled(!$can_edit)); 120 + 121 + if ($binding->getIsDisabled()) { 122 + $disable_icon = 'fa-check'; 123 + $disable_text = pht('Enable Binding'); 124 + } else { 125 + $disable_icon = 'fa-ban'; 126 + $disable_text = pht('Disable Binding'); 127 + } 128 + 129 + $disable_href = $this->getApplicationURI("binding/disable/{$id}/"); 130 + 131 + $actions->addAction( 132 + id(new PhabricatorActionView()) 133 + ->setIcon($disable_icon) 134 + ->setName($disable_text) 135 + ->setHref($disable_href) 136 + ->setWorkflow(true) 115 137 ->setDisabled(!$can_edit)); 116 138 117 139 return $actions;
+2 -1
src/applications/almanac/controller/AlmanacController.php
··· 177 177 $doc_link = phutil_tag( 178 178 'a', 179 179 array( 180 - 'href' => PhabricatorEnv::getDoclink('Almanac User Guide'), 180 + 'href' => PhabricatorEnv::getDoclink( 181 + 'User Guide: Phabricator Clusters'), 181 182 'target' => '_blank', 182 183 ), 183 184 pht('Learn More'));
+72
src/applications/almanac/controller/AlmanacInterfaceDeleteController.php
··· 1 + <?php 2 + 3 + final class AlmanacInterfaceDeleteController 4 + extends AlmanacDeviceController { 5 + 6 + public function handleRequest(AphrontRequest $request) { 7 + $viewer = $request->getViewer(); 8 + 9 + $id = $request->getURIData('id'); 10 + $interface = id(new AlmanacInterfaceQuery()) 11 + ->setViewer($viewer) 12 + ->withIDs(array($id)) 13 + ->requireCapabilities( 14 + array( 15 + PhabricatorPolicyCapability::CAN_VIEW, 16 + PhabricatorPolicyCapability::CAN_EDIT, 17 + )) 18 + ->executeOne(); 19 + if (!$interface) { 20 + return new Aphront404Response(); 21 + } 22 + 23 + $device = $interface->getDevice(); 24 + $device_uri = $device->getURI(); 25 + 26 + if ($interface->loadIsInUse()) { 27 + return $this->newDialog() 28 + ->setTitle(pht('Interface In Use')) 29 + ->appendParagraph( 30 + pht( 31 + 'You can not delete this interface because it is currently in '. 32 + 'use. One or more services are bound to it.')) 33 + ->addCancelButton($device_uri); 34 + } 35 + 36 + if ($request->isFormPost()) { 37 + $type_interface = AlmanacDeviceTransaction::TYPE_INTERFACE; 38 + 39 + $xactions = array(); 40 + 41 + $v_old = array( 42 + 'id' => $interface->getID(), 43 + ) + $interface->toAddress()->toDictionary(); 44 + 45 + $xactions[] = id(new AlmanacDeviceTransaction()) 46 + ->setTransactionType($type_interface) 47 + ->setOldValue($v_old) 48 + ->setNewValue(null); 49 + 50 + $editor = id(new AlmanacDeviceEditor()) 51 + ->setActor($viewer) 52 + ->setContentSourceFromRequest($request) 53 + ->setContinueOnNoEffect(true) 54 + ->setContinueOnMissingFields(true); 55 + 56 + $editor->applyTransactions($device, $xactions); 57 + 58 + return id(new AphrontRedirectResponse())->setURI($device_uri); 59 + } 60 + 61 + return $this->newDialog() 62 + ->setTitle(pht('Delete Interface')) 63 + ->appendParagraph( 64 + pht( 65 + 'Remove interface %s on device %s?', 66 + phutil_tag('strong', array(), $interface->renderDisplayAddress()), 67 + phutil_tag('strong', array(), $device->getName()))) 68 + ->addCancelButton($device_uri) 69 + ->addSubmitButton(pht('Delete Interface')); 70 + } 71 + 72 + }
+2 -1
src/applications/almanac/controller/AlmanacServiceViewController.php
··· 122 122 ->setNoDataString( 123 123 pht('This service has not been bound to any device interfaces yet.')) 124 124 ->setUser($viewer) 125 - ->setBindings($bindings); 125 + ->setBindings($bindings) 126 + ->setHideServiceColumn(true); 126 127 127 128 $header = id(new PHUIHeaderView()) 128 129 ->setHeader(pht('Service Bindings'))
+10
src/applications/almanac/editor/AlmanacBindingEditor.php
··· 13 13 $types = parent::getTransactionTypes(); 14 14 15 15 $types[] = AlmanacBindingTransaction::TYPE_INTERFACE; 16 + $types[] = AlmanacBindingTransaction::TYPE_DISABLE; 16 17 17 18 return $types; 18 19 } ··· 23 24 switch ($xaction->getTransactionType()) { 24 25 case AlmanacBindingTransaction::TYPE_INTERFACE: 25 26 return $object->getInterfacePHID(); 27 + case AlmanacBindingTransaction::TYPE_DISABLE: 28 + return $object->getIsDisabled(); 26 29 } 27 30 28 31 return parent::getCustomTransactionOldValue($object, $xaction); ··· 35 38 switch ($xaction->getTransactionType()) { 36 39 case AlmanacBindingTransaction::TYPE_INTERFACE: 37 40 return $xaction->getNewValue(); 41 + case AlmanacBindingTransaction::TYPE_DISABLE: 42 + return (int)$xaction->getNewValue(); 38 43 } 39 44 40 45 return parent::getCustomTransactionNewValue($object, $xaction); ··· 53 58 $object->setDevicePHID($interface->getDevicePHID()); 54 59 $object->setInterfacePHID($interface->getPHID()); 55 60 return; 61 + case AlmanacBindingTransaction::TYPE_DISABLE: 62 + $object->setIsDisabled($xaction->getNewValue()); 63 + return; 56 64 } 57 65 58 66 return parent::applyCustomInternalTransaction($object, $xaction); ··· 63 71 PhabricatorApplicationTransaction $xaction) { 64 72 65 73 switch ($xaction->getTransactionType()) { 74 + case AlmanacBindingTransaction::TYPE_DISABLE: 75 + return; 66 76 case AlmanacBindingTransaction::TYPE_INTERFACE: 67 77 $interface_phids = array(); 68 78
+13
src/applications/almanac/editor/AlmanacDeviceEditor.php
··· 310 310 pht('You can not edit an invalid or restricted interface.'), 311 311 $xaction); 312 312 $errors[] = $error; 313 + continue; 314 + } 315 + 316 + $new = $xaction->getNewValue(); 317 + if (!$new) { 318 + if ($interface->loadIsInUse()) { 319 + $error = new PhabricatorApplicationTransactionValidationError( 320 + $type, 321 + pht('In Use'), 322 + pht('You can not delete an interface which is still in use.'), 323 + $xaction); 324 + $errors[] = $error; 325 + } 313 326 } 314 327 } 315 328 }
+1
src/applications/almanac/engineextension/AlmanacSearchEngineAttachment.php
··· 28 28 'phid' => $binding->getPHID(), 29 29 'properties' => $this->getAlmanacPropertyList($binding), 30 30 'interface' => $this->getAlmanacInterfaceDictionary($interface), 31 + 'disabled' => (bool)$binding->getIsDisabled(), 31 32 ); 32 33 } 33 34
+4 -1
src/applications/almanac/storage/AlmanacBinding.php
··· 13 13 protected $devicePHID; 14 14 protected $interfacePHID; 15 15 protected $mailKey; 16 + protected $isDisabled; 16 17 17 18 private $service = self::ATTACHABLE; 18 19 private $device = self::ATTACHABLE; ··· 22 23 public static function initializeNewBinding(AlmanacService $service) { 23 24 return id(new AlmanacBinding()) 24 25 ->setServicePHID($service->getPHID()) 25 - ->attachAlmanacProperties(array()); 26 + ->attachAlmanacProperties(array()) 27 + ->setIsDisabled(0); 26 28 } 27 29 28 30 protected function getConfiguration() { ··· 30 32 self::CONFIG_AUX_PHID => true, 31 33 self::CONFIG_COLUMN_SCHEMA => array( 32 34 'mailKey' => 'bytes20', 35 + 'isDisabled' => 'bool', 33 36 ), 34 37 self::CONFIG_KEY_SCHEMA => array( 35 38 'key_service' => array(
+12
src/applications/almanac/storage/AlmanacBindingTransaction.php
··· 4 4 extends AlmanacTransaction { 5 5 6 6 const TYPE_INTERFACE = 'almanac:binding:interface'; 7 + const TYPE_DISABLE = 'almanac:binding:disable'; 7 8 8 9 public function getApplicationName() { 9 10 return 'almanac'; ··· 55 56 $this->renderHandleLink($author_phid), 56 57 $this->renderHandleLink($old), 57 58 $this->renderHandleLink($new)); 59 + } 60 + break; 61 + case self::TYPE_DISABLE: 62 + if ($new) { 63 + return pht( 64 + '%s disabled this binding.', 65 + $this->renderHandleLink($author_phid)); 66 + } else { 67 + return pht( 68 + '%s enabled this binding.', 69 + $this->renderHandleLink($author_phid)); 58 70 } 59 71 break; 60 72 }
+1 -1
src/applications/almanac/storage/AlmanacDeviceTransaction.php
··· 69 69 return pht( 70 70 '%s removed the interface %s from this device.', 71 71 $this->renderHandleLink($author_phid), 72 - $this->describeInterface($new)); 72 + $this->describeInterface($old)); 73 73 } else if ($new) { 74 74 return pht( 75 75 '%s added the interface %s to this device.',
+33 -1
src/applications/almanac/storage/AlmanacInterface.php
··· 4 4 extends AlmanacDAO 5 5 implements 6 6 PhabricatorPolicyInterface, 7 - PhabricatorDestructibleInterface { 7 + PhabricatorDestructibleInterface, 8 + PhabricatorExtendedPolicyInterface { 8 9 9 10 protected $devicePHID; 10 11 protected $networkPHID; ··· 74 75 return $this->getAddress().':'.$this->getPort(); 75 76 } 76 77 78 + public function loadIsInUse() { 79 + $binding = id(new AlmanacBindingQuery()) 80 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 81 + ->withInterfacePHIDs(array($this->getPHID())) 82 + ->setLimit(1) 83 + ->executeOne(); 84 + 85 + return (bool)$binding; 86 + } 87 + 77 88 78 89 /* -( PhabricatorPolicyInterface )----------------------------------------- */ 79 90 ··· 102 113 ); 103 114 104 115 return $notes; 116 + } 117 + 118 + 119 + /* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ 120 + 121 + 122 + public function getExtendedPolicy($capability, PhabricatorUser $viewer) { 123 + switch ($capability) { 124 + case PhabricatorPolicyCapability::CAN_EDIT: 125 + if ($this->getDevice()->isClusterDevice()) { 126 + return array( 127 + array( 128 + new PhabricatorAlmanacApplication(), 129 + AlmanacManageClusterServicesCapability::CAPABILITY, 130 + ), 131 + ); 132 + } 133 + break; 134 + } 135 + 136 + return array(); 105 137 } 106 138 107 139
+36
src/applications/almanac/view/AlmanacBindingTableView.php
··· 5 5 private $bindings; 6 6 private $noDataString; 7 7 8 + private $hideServiceColumn; 9 + 8 10 public function setNoDataString($no_data_string) { 9 11 $this->noDataString = $no_data_string; 10 12 return $this; ··· 23 25 return $this->bindings; 24 26 } 25 27 28 + public function setHideServiceColumn($hide_service_column) { 29 + $this->hideServiceColumn = $hide_service_column; 30 + return $this; 31 + } 32 + 33 + public function getHideServiceColumn() { 34 + return $this->hideServiceColumn; 35 + } 36 + 26 37 public function render() { 27 38 $bindings = $this->getBindings(); 28 39 $viewer = $this->getUser(); ··· 35 46 } 36 47 $handles = $viewer->loadHandles($phids); 37 48 49 + $icon_disabled = id(new PHUIIconView()) 50 + ->setIcon('fa-ban') 51 + ->addSigil('has-tooltip') 52 + ->setMetadata( 53 + array( 54 + 'tip' => pht('Disabled'), 55 + )); 56 + 57 + $icon_active = id(new PHUIIconView()) 58 + ->setIcon('fa-check') 59 + ->addSigil('has-tooltip') 60 + ->setMetadata( 61 + array( 62 + 'tip' => pht('Active'), 63 + )); 64 + 38 65 $rows = array(); 39 66 foreach ($bindings as $binding) { 40 67 $addr = $binding->getInterface()->getAddress(); ··· 42 69 43 70 $rows[] = array( 44 71 $binding->getID(), 72 + ($binding->getIsDisabled() ? $icon_disabled : $icon_active), 45 73 $handles->renderHandle($binding->getServicePHID()), 46 74 $handles->renderHandle($binding->getDevicePHID()), 47 75 $handles->renderHandle($binding->getInterface()->getNetworkPHID()), ··· 61 89 ->setHeaders( 62 90 array( 63 91 pht('ID'), 92 + null, 64 93 pht('Service'), 65 94 pht('Device'), 66 95 pht('Network'), ··· 70 99 ->setColumnClasses( 71 100 array( 72 101 '', 102 + 'icon', 73 103 '', 74 104 '', 75 105 '', 76 106 'wide', 77 107 'action', 108 + )) 109 + ->setColumnVisibility( 110 + array( 111 + true, 112 + true, 113 + !$this->getHideServiceColumn(), 78 114 )); 79 115 80 116 return $table;
+15 -2
src/applications/almanac/view/AlmanacInterfaceTableView.php
··· 27 27 $interfaces = $this->getInterfaces(); 28 28 $viewer = $this->getUser(); 29 29 30 - if ($this->getCanEdit()) { 30 + $can_edit = $this->getCanEdit(); 31 + 32 + if ($can_edit) { 31 33 $button_class = 'small grey button'; 32 34 } else { 33 35 $button_class = 'small grey button disabled'; ··· 42 44 $handles->renderHandle($interface->getNetworkPHID()), 43 45 $interface->getAddress(), 44 46 $interface->getPort(), 45 - phutil_tag( 47 + javelin_tag( 46 48 'a', 47 49 array( 48 50 'class' => $button_class, 49 51 'href' => '/almanac/interface/edit/'.$interface->getID().'/', 52 + 'sigil' => ($can_edit ? null : 'workflow'), 50 53 ), 51 54 pht('Edit')), 55 + javelin_tag( 56 + 'a', 57 + array( 58 + 'class' => $button_class, 59 + 'href' => '/almanac/interface/delete/'.$interface->getID().'/', 60 + 'sigil' => 'workflow', 61 + ), 62 + pht('Delete')), 52 63 ); 53 64 } 54 65 ··· 60 71 pht('Address'), 61 72 pht('Port'), 62 73 null, 74 + null, 63 75 )) 64 76 ->setColumnClasses( 65 77 array( ··· 67 79 'wide', 68 80 '', 69 81 '', 82 + 'action', 70 83 'action', 71 84 )); 72 85
+5
src/applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php
··· 267 267 268 268 $free = array(); 269 269 foreach ($bindings as $binding) { 270 + // Don't consider disabled bindings to be available. 271 + if ($binding->getIsDisabled()) { 272 + continue; 273 + } 274 + 270 275 if (empty($allocated_phids[$binding->getPHID()])) { 271 276 $free[] = $binding; 272 277 }
+38 -6
src/docs/user/configuration/cluster.diviner
··· 1 1 @title User Guide: Phabricator Clusters 2 2 @group config 3 3 4 - Guide on scaling Phabricator across multiple machines, for large installs. 4 + Guide on scaling Phabricator across multiple machines. 5 5 6 6 Overview 7 7 ======== ··· 9 9 IMPORTANT: Phabricator clustering is in its infancy and does not work at all 10 10 yet. This document is mostly a placeholder. 11 11 12 - Locking Services 13 - ================ 12 + IMPORTANT: DO NOT CONFIGURE CLUSTER SERVICES UNLESS YOU HAVE **TWENTY YEARS OF 13 + EXPERIENCE WITH PHABRICATOR** AND **A MINIMUM OF 17 PHABRICATOR PHDs**. YOU 14 + WILL BREAK YOUR INSTALL AND BE UNABLE TO REPAIR IT. 14 15 15 - Very briefly, you can set "Can Manage Cluster Services" to "No One" to lock 16 - the cluster definition. 16 + See also @{article:Almanac User Guide}. 17 17 18 - See also @{article:Almanac User Guide}. 18 + 19 + Managing Cluster Configuration 20 + ============================== 21 + 22 + Cluster configuration is managed primarily from the **Almanac** application. 23 + 24 + To define cluster services and create or edit cluster configuration, you must 25 + have the **Can Manage Cluster Services** application permission in Almanac. If 26 + you do not have this permission, all cluster services and all connected devices 27 + will be locked and not editable. 28 + 29 + The **Can Manage Cluster Services** permission is stronger than service and 30 + device policies, and overrides them. You can never edit a cluster service if 31 + you don't have this permission, even if the **Can Edit** policy on the service 32 + itself is very permissive. 33 + 34 + 35 + Locking Cluster Configuration 36 + ============================= 37 + 38 + IMPORTANT: Managing cluster services is **dangerous** and **fragile**. 39 + 40 + If you make a mistake, you can break your install. Because the install is 41 + broken, you will be unable to load the web interface in order to repair it. 42 + 43 + IMPORTANT: Currently, broken clusters must be repaired by manually fixing them 44 + in the database. There are no instructions available on how to do this, and no 45 + tools to help you. Do not configure cluster services. 46 + 47 + If an attacker gains access to an account with permission to manage cluster 48 + services, they can add devices they control as database servers. These servers 49 + will then receive sensitive data and traffic, and allow the attacker to 50 + escalate their access and completely compromise an install.