diff --git a/resources/sql/autopatches/20160225.almanac.1.disablebinding.sql b/resources/sql/autopatches/20160225.almanac.1.disablebinding.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20160225.almanac.1.disablebinding.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_almanac.almanac_binding + ADD isDisabled BOOL NOT NULL; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -11,6 +11,7 @@ 'class' => array( 'AlmanacAddress' => 'applications/almanac/util/AlmanacAddress.php', 'AlmanacBinding' => 'applications/almanac/storage/AlmanacBinding.php', + 'AlmanacBindingDisableController' => 'applications/almanac/controller/AlmanacBindingDisableController.php', 'AlmanacBindingEditController' => 'applications/almanac/controller/AlmanacBindingEditController.php', 'AlmanacBindingEditor' => 'applications/almanac/editor/AlmanacBindingEditor.php', 'AlmanacBindingPHIDType' => 'applications/almanac/phid/AlmanacBindingPHIDType.php', @@ -51,6 +52,7 @@ 'AlmanacEditor' => 'applications/almanac/editor/AlmanacEditor.php', 'AlmanacInterface' => 'applications/almanac/storage/AlmanacInterface.php', 'AlmanacInterfaceDatasource' => 'applications/almanac/typeahead/AlmanacInterfaceDatasource.php', + 'AlmanacInterfaceDeleteController' => 'applications/almanac/controller/AlmanacInterfaceDeleteController.php', 'AlmanacInterfaceEditController' => 'applications/almanac/controller/AlmanacInterfaceEditController.php', 'AlmanacInterfacePHIDType' => 'applications/almanac/phid/AlmanacInterfacePHIDType.php', 'AlmanacInterfaceQuery' => 'applications/almanac/query/AlmanacInterfaceQuery.php', @@ -3996,6 +3998,7 @@ 'PhabricatorDestructibleInterface', 'PhabricatorExtendedPolicyInterface', ), + 'AlmanacBindingDisableController' => 'AlmanacServiceController', 'AlmanacBindingEditController' => 'AlmanacServiceController', 'AlmanacBindingEditor' => 'AlmanacEditor', 'AlmanacBindingPHIDType' => 'PhabricatorPHIDType', @@ -4049,8 +4052,10 @@ 'AlmanacDAO', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', + 'PhabricatorExtendedPolicyInterface', ), 'AlmanacInterfaceDatasource' => 'PhabricatorTypeaheadDatasource', + 'AlmanacInterfaceDeleteController' => 'AlmanacDeviceController', 'AlmanacInterfaceEditController' => 'AlmanacDeviceController', 'AlmanacInterfacePHIDType' => 'PhabricatorPHIDType', 'AlmanacInterfaceQuery' => 'AlmanacQuery', diff --git a/src/applications/almanac/application/PhabricatorAlmanacApplication.php b/src/applications/almanac/application/PhabricatorAlmanacApplication.php --- a/src/applications/almanac/application/PhabricatorAlmanacApplication.php +++ b/src/applications/almanac/application/PhabricatorAlmanacApplication.php @@ -55,9 +55,11 @@ ), 'interface/' => array( 'edit/(?:(?P\d+)/)?' => 'AlmanacInterfaceEditController', + 'delete/(?:(?P\d+)/)?' => 'AlmanacInterfaceDeleteController', ), 'binding/' => array( 'edit/(?:(?P\d+)/)?' => 'AlmanacBindingEditController', + 'disable/(?:(?P\d+)/)?' => 'AlmanacBindingDisableController', '(?P\d+)/' => 'AlmanacBindingViewController', ), 'network/' => array( @@ -80,6 +82,17 @@ } protected function getCustomCapabilities() { + $cluster_caption = pht( + 'This permission is very dangerous. %s', + phutil_tag( + 'a', + array( + 'href' => PhabricatorEnv::getDoclink( + 'User Guide: Phabricator Clusters'), + 'target' => '_blank', + ), + pht('Learn More'))); + return array( AlmanacCreateServicesCapability::CAPABILITY => array( 'default' => PhabricatorPolicies::POLICY_ADMIN, @@ -94,7 +107,8 @@ 'default' => PhabricatorPolicies::POLICY_ADMIN, ), AlmanacManageClusterServicesCapability::CAPABILITY => array( - 'default' => PhabricatorPolicies::POLICY_ADMIN, + 'default' => PhabricatorPolicies::POLICY_NOONE, + 'caption' => $cluster_caption, ), ); } diff --git a/src/applications/almanac/controller/AlmanacBindingDisableController.php b/src/applications/almanac/controller/AlmanacBindingDisableController.php new file mode 100644 --- /dev/null +++ b/src/applications/almanac/controller/AlmanacBindingDisableController.php @@ -0,0 +1,69 @@ +getViewer(); + + $id = $request->getURIData('id'); + $binding = id(new AlmanacBindingQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$binding) { + return new Aphront404Response(); + } + + $id = $binding->getID(); + $is_disable = !$binding->getIsDisabled(); + $done_uri = $binding->getURI(); + + if ($is_disable) { + $disable_title = pht('Disable Binding'); + $disable_body = pht('Disable this binding?'); + $disable_button = pht('Disable Binding'); + + $v_disable = 1; + } else { + $disable_title = pht('Enable Binding'); + $disable_body = pht('Enable this binding?'); + $disable_button = pht('Enable Binding'); + + $v_disable = 0; + } + + + if ($request->isFormPost()) { + $type_disable = AlmanacBindingTransaction::TYPE_DISABLE; + + $xactions = array(); + + $xactions[] = id(new AlmanacBindingTransaction()) + ->setTransactionType($type_disable) + ->setNewValue($v_disable); + + $editor = id(new AlmanacBindingEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true); + + $editor->applyTransactions($binding, $xactions); + + return id(new AphrontRedirectResponse())->setURI($done_uri); + } + + return $this->newDialog() + ->setTitle($disable_title) + ->appendParagraph($disable_body) + ->addSubmitButton($disable_button) + ->addCancelButton($done_uri); + } + +} diff --git a/src/applications/almanac/controller/AlmanacBindingViewController.php b/src/applications/almanac/controller/AlmanacBindingViewController.php --- a/src/applications/almanac/controller/AlmanacBindingViewController.php +++ b/src/applications/almanac/controller/AlmanacBindingViewController.php @@ -35,6 +35,10 @@ ->setHeader($title) ->setPolicyObject($binding); + if ($binding->getIsDisabled()) { + $header->setStatus('fa-ban', 'red', pht('Disabled')); + } + $box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($property_list); @@ -114,6 +118,24 @@ ->setWorkflow(!$can_edit) ->setDisabled(!$can_edit)); + if ($binding->getIsDisabled()) { + $disable_icon = 'fa-check'; + $disable_text = pht('Enable Binding'); + } else { + $disable_icon = 'fa-ban'; + $disable_text = pht('Disable Binding'); + } + + $disable_href = $this->getApplicationURI("binding/disable/{$id}/"); + + $actions->addAction( + id(new PhabricatorActionView()) + ->setIcon($disable_icon) + ->setName($disable_text) + ->setHref($disable_href) + ->setWorkflow(true) + ->setDisabled(!$can_edit)); + return $actions; } diff --git a/src/applications/almanac/controller/AlmanacController.php b/src/applications/almanac/controller/AlmanacController.php --- a/src/applications/almanac/controller/AlmanacController.php +++ b/src/applications/almanac/controller/AlmanacController.php @@ -177,7 +177,8 @@ $doc_link = phutil_tag( 'a', array( - 'href' => PhabricatorEnv::getDoclink('Almanac User Guide'), + 'href' => PhabricatorEnv::getDoclink( + 'User Guide: Phabricator Clusters'), 'target' => '_blank', ), pht('Learn More')); diff --git a/src/applications/almanac/controller/AlmanacInterfaceDeleteController.php b/src/applications/almanac/controller/AlmanacInterfaceDeleteController.php new file mode 100644 --- /dev/null +++ b/src/applications/almanac/controller/AlmanacInterfaceDeleteController.php @@ -0,0 +1,72 @@ +getViewer(); + + $id = $request->getURIData('id'); + $interface = id(new AlmanacInterfaceQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$interface) { + return new Aphront404Response(); + } + + $device = $interface->getDevice(); + $device_uri = $device->getURI(); + + if ($interface->loadIsInUse()) { + return $this->newDialog() + ->setTitle(pht('Interface In Use')) + ->appendParagraph( + pht( + 'You can not delete this interface because it is currently in '. + 'use. One or more services are bound to it.')) + ->addCancelButton($device_uri); + } + + if ($request->isFormPost()) { + $type_interface = AlmanacDeviceTransaction::TYPE_INTERFACE; + + $xactions = array(); + + $v_old = array( + 'id' => $interface->getID(), + ) + $interface->toAddress()->toDictionary(); + + $xactions[] = id(new AlmanacDeviceTransaction()) + ->setTransactionType($type_interface) + ->setOldValue($v_old) + ->setNewValue(null); + + $editor = id(new AlmanacDeviceEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true); + + $editor->applyTransactions($device, $xactions); + + return id(new AphrontRedirectResponse())->setURI($device_uri); + } + + return $this->newDialog() + ->setTitle(pht('Delete Interface')) + ->appendParagraph( + pht( + 'Remove interface %s on device %s?', + phutil_tag('strong', array(), $interface->renderDisplayAddress()), + phutil_tag('strong', array(), $device->getName()))) + ->addCancelButton($device_uri) + ->addSubmitButton(pht('Delete Interface')); + } + +} diff --git a/src/applications/almanac/controller/AlmanacServiceViewController.php b/src/applications/almanac/controller/AlmanacServiceViewController.php --- a/src/applications/almanac/controller/AlmanacServiceViewController.php +++ b/src/applications/almanac/controller/AlmanacServiceViewController.php @@ -122,7 +122,8 @@ ->setNoDataString( pht('This service has not been bound to any device interfaces yet.')) ->setUser($viewer) - ->setBindings($bindings); + ->setBindings($bindings) + ->setHideServiceColumn(true); $header = id(new PHUIHeaderView()) ->setHeader(pht('Service Bindings')) diff --git a/src/applications/almanac/editor/AlmanacBindingEditor.php b/src/applications/almanac/editor/AlmanacBindingEditor.php --- a/src/applications/almanac/editor/AlmanacBindingEditor.php +++ b/src/applications/almanac/editor/AlmanacBindingEditor.php @@ -13,6 +13,7 @@ $types = parent::getTransactionTypes(); $types[] = AlmanacBindingTransaction::TYPE_INTERFACE; + $types[] = AlmanacBindingTransaction::TYPE_DISABLE; return $types; } @@ -23,6 +24,8 @@ switch ($xaction->getTransactionType()) { case AlmanacBindingTransaction::TYPE_INTERFACE: return $object->getInterfacePHID(); + case AlmanacBindingTransaction::TYPE_DISABLE: + return $object->getIsDisabled(); } return parent::getCustomTransactionOldValue($object, $xaction); @@ -35,6 +38,8 @@ switch ($xaction->getTransactionType()) { case AlmanacBindingTransaction::TYPE_INTERFACE: return $xaction->getNewValue(); + case AlmanacBindingTransaction::TYPE_DISABLE: + return (int)$xaction->getNewValue(); } return parent::getCustomTransactionNewValue($object, $xaction); @@ -53,6 +58,9 @@ $object->setDevicePHID($interface->getDevicePHID()); $object->setInterfacePHID($interface->getPHID()); return; + case AlmanacBindingTransaction::TYPE_DISABLE: + $object->setIsDisabled($xaction->getNewValue()); + return; } return parent::applyCustomInternalTransaction($object, $xaction); @@ -63,6 +71,8 @@ PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { + case AlmanacBindingTransaction::TYPE_DISABLE: + return; case AlmanacBindingTransaction::TYPE_INTERFACE: $interface_phids = array(); diff --git a/src/applications/almanac/editor/AlmanacDeviceEditor.php b/src/applications/almanac/editor/AlmanacDeviceEditor.php --- a/src/applications/almanac/editor/AlmanacDeviceEditor.php +++ b/src/applications/almanac/editor/AlmanacDeviceEditor.php @@ -310,6 +310,19 @@ pht('You can not edit an invalid or restricted interface.'), $xaction); $errors[] = $error; + continue; + } + + $new = $xaction->getNewValue(); + if (!$new) { + if ($interface->loadIsInUse()) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('In Use'), + pht('You can not delete an interface which is still in use.'), + $xaction); + $errors[] = $error; + } } } } diff --git a/src/applications/almanac/engineextension/AlmanacSearchEngineAttachment.php b/src/applications/almanac/engineextension/AlmanacSearchEngineAttachment.php --- a/src/applications/almanac/engineextension/AlmanacSearchEngineAttachment.php +++ b/src/applications/almanac/engineextension/AlmanacSearchEngineAttachment.php @@ -28,6 +28,7 @@ 'phid' => $binding->getPHID(), 'properties' => $this->getAlmanacPropertyList($binding), 'interface' => $this->getAlmanacInterfaceDictionary($interface), + 'disabled' => (bool)$binding->getIsDisabled(), ); } diff --git a/src/applications/almanac/storage/AlmanacBinding.php b/src/applications/almanac/storage/AlmanacBinding.php --- a/src/applications/almanac/storage/AlmanacBinding.php +++ b/src/applications/almanac/storage/AlmanacBinding.php @@ -13,6 +13,7 @@ protected $devicePHID; protected $interfacePHID; protected $mailKey; + protected $isDisabled; private $service = self::ATTACHABLE; private $device = self::ATTACHABLE; @@ -22,7 +23,8 @@ public static function initializeNewBinding(AlmanacService $service) { return id(new AlmanacBinding()) ->setServicePHID($service->getPHID()) - ->attachAlmanacProperties(array()); + ->attachAlmanacProperties(array()) + ->setIsDisabled(0); } protected function getConfiguration() { @@ -30,6 +32,7 @@ self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'mailKey' => 'bytes20', + 'isDisabled' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_service' => array( diff --git a/src/applications/almanac/storage/AlmanacBindingTransaction.php b/src/applications/almanac/storage/AlmanacBindingTransaction.php --- a/src/applications/almanac/storage/AlmanacBindingTransaction.php +++ b/src/applications/almanac/storage/AlmanacBindingTransaction.php @@ -4,6 +4,7 @@ extends AlmanacTransaction { const TYPE_INTERFACE = 'almanac:binding:interface'; + const TYPE_DISABLE = 'almanac:binding:disable'; public function getApplicationName() { return 'almanac'; @@ -57,6 +58,17 @@ $this->renderHandleLink($new)); } break; + case self::TYPE_DISABLE: + if ($new) { + return pht( + '%s disabled this binding.', + $this->renderHandleLink($author_phid)); + } else { + return pht( + '%s enabled this binding.', + $this->renderHandleLink($author_phid)); + } + break; } return parent::getTitle(); diff --git a/src/applications/almanac/storage/AlmanacDeviceTransaction.php b/src/applications/almanac/storage/AlmanacDeviceTransaction.php --- a/src/applications/almanac/storage/AlmanacDeviceTransaction.php +++ b/src/applications/almanac/storage/AlmanacDeviceTransaction.php @@ -69,7 +69,7 @@ return pht( '%s removed the interface %s from this device.', $this->renderHandleLink($author_phid), - $this->describeInterface($new)); + $this->describeInterface($old)); } else if ($new) { return pht( '%s added the interface %s to this device.', diff --git a/src/applications/almanac/storage/AlmanacInterface.php b/src/applications/almanac/storage/AlmanacInterface.php --- a/src/applications/almanac/storage/AlmanacInterface.php +++ b/src/applications/almanac/storage/AlmanacInterface.php @@ -4,7 +4,8 @@ extends AlmanacDAO implements PhabricatorPolicyInterface, - PhabricatorDestructibleInterface { + PhabricatorDestructibleInterface, + PhabricatorExtendedPolicyInterface { protected $devicePHID; protected $networkPHID; @@ -74,6 +75,16 @@ return $this->getAddress().':'.$this->getPort(); } + public function loadIsInUse() { + $binding = id(new AlmanacBindingQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withInterfacePHIDs(array($this->getPHID())) + ->setLimit(1) + ->executeOne(); + + return (bool)$binding; + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ @@ -105,6 +116,27 @@ } +/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ + + + public function getExtendedPolicy($capability, PhabricatorUser $viewer) { + switch ($capability) { + case PhabricatorPolicyCapability::CAN_EDIT: + if ($this->getDevice()->isClusterDevice()) { + return array( + array( + new PhabricatorAlmanacApplication(), + AlmanacManageClusterServicesCapability::CAPABILITY, + ), + ); + } + break; + } + + return array(); + } + + /* -( PhabricatorDestructibleInterface )----------------------------------- */ diff --git a/src/applications/almanac/view/AlmanacBindingTableView.php b/src/applications/almanac/view/AlmanacBindingTableView.php --- a/src/applications/almanac/view/AlmanacBindingTableView.php +++ b/src/applications/almanac/view/AlmanacBindingTableView.php @@ -5,6 +5,8 @@ private $bindings; private $noDataString; + private $hideServiceColumn; + public function setNoDataString($no_data_string) { $this->noDataString = $no_data_string; return $this; @@ -23,6 +25,15 @@ return $this->bindings; } + public function setHideServiceColumn($hide_service_column) { + $this->hideServiceColumn = $hide_service_column; + return $this; + } + + public function getHideServiceColumn() { + return $this->hideServiceColumn; + } + public function render() { $bindings = $this->getBindings(); $viewer = $this->getUser(); @@ -35,6 +46,22 @@ } $handles = $viewer->loadHandles($phids); + $icon_disabled = id(new PHUIIconView()) + ->setIcon('fa-ban') + ->addSigil('has-tooltip') + ->setMetadata( + array( + 'tip' => pht('Disabled'), + )); + + $icon_active = id(new PHUIIconView()) + ->setIcon('fa-check') + ->addSigil('has-tooltip') + ->setMetadata( + array( + 'tip' => pht('Active'), + )); + $rows = array(); foreach ($bindings as $binding) { $addr = $binding->getInterface()->getAddress(); @@ -42,6 +69,7 @@ $rows[] = array( $binding->getID(), + ($binding->getIsDisabled() ? $icon_disabled : $icon_active), $handles->renderHandle($binding->getServicePHID()), $handles->renderHandle($binding->getDevicePHID()), $handles->renderHandle($binding->getInterface()->getNetworkPHID()), @@ -61,6 +89,7 @@ ->setHeaders( array( pht('ID'), + null, pht('Service'), pht('Device'), pht('Network'), @@ -70,11 +99,18 @@ ->setColumnClasses( array( '', + 'icon', '', '', '', 'wide', 'action', + )) + ->setColumnVisibility( + array( + true, + true, + !$this->getHideServiceColumn(), )); return $table; diff --git a/src/applications/almanac/view/AlmanacInterfaceTableView.php b/src/applications/almanac/view/AlmanacInterfaceTableView.php --- a/src/applications/almanac/view/AlmanacInterfaceTableView.php +++ b/src/applications/almanac/view/AlmanacInterfaceTableView.php @@ -27,7 +27,9 @@ $interfaces = $this->getInterfaces(); $viewer = $this->getUser(); - if ($this->getCanEdit()) { + $can_edit = $this->getCanEdit(); + + if ($can_edit) { $button_class = 'small grey button'; } else { $button_class = 'small grey button disabled'; @@ -42,13 +44,22 @@ $handles->renderHandle($interface->getNetworkPHID()), $interface->getAddress(), $interface->getPort(), - phutil_tag( + javelin_tag( 'a', array( 'class' => $button_class, 'href' => '/almanac/interface/edit/'.$interface->getID().'/', + 'sigil' => ($can_edit ? null : 'workflow'), ), pht('Edit')), + javelin_tag( + 'a', + array( + 'class' => $button_class, + 'href' => '/almanac/interface/delete/'.$interface->getID().'/', + 'sigil' => 'workflow', + ), + pht('Delete')), ); } @@ -60,6 +71,7 @@ pht('Address'), pht('Port'), null, + null, )) ->setColumnClasses( array( @@ -68,6 +80,7 @@ '', '', 'action', + 'action', )); return $table; diff --git a/src/applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php --- a/src/applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php +++ b/src/applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php @@ -267,6 +267,11 @@ $free = array(); foreach ($bindings as $binding) { + // Don't consider disabled bindings to be available. + if ($binding->getIsDisabled()) { + continue; + } + if (empty($allocated_phids[$binding->getPHID()])) { $free[] = $binding; } diff --git a/src/docs/user/configuration/cluster.diviner b/src/docs/user/configuration/cluster.diviner --- a/src/docs/user/configuration/cluster.diviner +++ b/src/docs/user/configuration/cluster.diviner @@ -1,7 +1,7 @@ @title User Guide: Phabricator Clusters @group config -Guide on scaling Phabricator across multiple machines, for large installs. +Guide on scaling Phabricator across multiple machines. Overview ======== @@ -9,10 +9,42 @@ IMPORTANT: Phabricator clustering is in its infancy and does not work at all yet. This document is mostly a placeholder. -Locking Services -================ - -Very briefly, you can set "Can Manage Cluster Services" to "No One" to lock -the cluster definition. +IMPORTANT: DO NOT CONFIGURE CLUSTER SERVICES UNLESS YOU HAVE **TWENTY YEARS OF +EXPERIENCE WITH PHABRICATOR** AND **A MINIMUM OF 17 PHABRICATOR PHDs**. YOU +WILL BREAK YOUR INSTALL AND BE UNABLE TO REPAIR IT. See also @{article:Almanac User Guide}. + + +Managing Cluster Configuration +============================== + +Cluster configuration is managed primarily from the **Almanac** application. + +To define cluster services and create or edit cluster configuration, you must +have the **Can Manage Cluster Services** application permission in Almanac. If +you do not have this permission, all cluster services and all connected devices +will be locked and not editable. + +The **Can Manage Cluster Services** permission is stronger than service and +device policies, and overrides them. You can never edit a cluster service if +you don't have this permission, even if the **Can Edit** policy on the service +itself is very permissive. + + +Locking Cluster Configuration +============================= + +IMPORTANT: Managing cluster services is **dangerous** and **fragile**. + +If you make a mistake, you can break your install. Because the install is +broken, you will be unable to load the web interface in order to repair it. + +IMPORTANT: Currently, broken clusters must be repaired by manually fixing them +in the database. There are no instructions available on how to do this, and no +tools to help you. Do not configure cluster services. + +If an attacker gains access to an account with permission to manage cluster +services, they can add devices they control as database servers. These servers +will then receive sensitive data and traffic, and allow the attacker to +escalate their access and completely compromise an install.