diff --git a/resources/sql/autopatches/20141217.almanacdevicelock.sql b/resources/sql/autopatches/20141217.almanacdevicelock.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20141217.almanacdevicelock.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_almanac.almanac_device + ADD isLocked BOOL NOT NULL; diff --git a/resources/sql/autopatches/20141217.almanaclock.sql b/resources/sql/autopatches/20141217.almanaclock.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20141217.almanaclock.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_almanac.almanac_service + ADD isLocked 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 @@ -49,7 +49,9 @@ 'AlmanacInterfacePHIDType' => 'applications/almanac/phid/AlmanacInterfacePHIDType.php', 'AlmanacInterfaceQuery' => 'applications/almanac/query/AlmanacInterfaceQuery.php', 'AlmanacInterfaceTableView' => 'applications/almanac/view/AlmanacInterfaceTableView.php', + 'AlmanacManagementLockWorkflow' => 'applications/almanac/management/AlmanacManagementLockWorkflow.php', 'AlmanacManagementTrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementTrustKeyWorkflow.php', + 'AlmanacManagementUnlockWorkflow' => 'applications/almanac/management/AlmanacManagementUnlockWorkflow.php', 'AlmanacManagementUntrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementUntrustKeyWorkflow.php', 'AlmanacManagementWorkflow' => 'applications/almanac/management/AlmanacManagementWorkflow.php', 'AlmanacNames' => 'applications/almanac/util/AlmanacNames.php', @@ -3068,7 +3070,9 @@ 'AlmanacInterfacePHIDType' => 'PhabricatorPHIDType', 'AlmanacInterfaceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'AlmanacInterfaceTableView' => 'AphrontView', + 'AlmanacManagementLockWorkflow' => 'AlmanacManagementWorkflow', 'AlmanacManagementTrustKeyWorkflow' => 'AlmanacManagementWorkflow', + 'AlmanacManagementUnlockWorkflow' => 'AlmanacManagementWorkflow', 'AlmanacManagementUntrustKeyWorkflow' => 'AlmanacManagementWorkflow', 'AlmanacManagementWorkflow' => 'PhabricatorManagementWorkflow', 'AlmanacNames' => 'Phobject', 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 @@ -26,6 +26,10 @@ return self::GROUP_UTILITIES; } + public function getHelpURI() { + return PhabricatorEnv::getDoclink('Almanac User Guide'); + } + public function isPrototype() { return true; } 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 @@ -38,6 +38,14 @@ ->setHeader($header) ->addPropertyList($property_list); + if ($binding->getService()->getIsLocked()) { + $this->addLockMessage( + $box, + pht( + 'This service for this binding is locked, so the binding can '. + 'not be edited.')); + } + $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($service->getName(), $service_uri); $crumbs->addTextCrumb($title); 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 @@ -179,4 +179,23 @@ ->appendChild($table); } + protected function addLockMessage(PHUIObjectBoxView $box, $message) { + $doc_link = phutil_tag( + 'a', + array( + 'href' => PhabricatorEnv::getDoclink('Almanac User Guide'), + 'target' => '_blank', + ), + pht('Learn More')); + + $error_view = id(new AphrontErrorView()) + ->setSeverity(AphrontErrorView::SEVERITY_WARNING) + ->setErrors( + array( + array($message, ' ', $doc_link), + )); + + $box->setErrorView($error_view); + } + } diff --git a/src/applications/almanac/controller/AlmanacDeviceViewController.php b/src/applications/almanac/controller/AlmanacDeviceViewController.php --- a/src/applications/almanac/controller/AlmanacDeviceViewController.php +++ b/src/applications/almanac/controller/AlmanacDeviceViewController.php @@ -20,6 +20,10 @@ return new Aphront404Response(); } + // We rebuild locks on a device when viewing the detail page, so they + // automatically get corrected if they fall out of sync. + $device->rebuildDeviceLocks(); + $title = pht('Device %s', $device->getName()); $property_list = $this->buildPropertyList($device); @@ -35,6 +39,14 @@ ->setHeader($header) ->addPropertyList($property_list); + if ($device->getIsLocked()) { + $this->addLockMessage( + $box, + pht( + 'This device is bound to a locked service, so it can not be '. + 'edited.')); + } + $interfaces = $this->buildInterfaceList($device); $crumbs = $this->buildApplicationCrumbs(); @@ -52,6 +64,7 @@ $interfaces, $this->buildAlmanacPropertiesTable($device), $this->buildSSHKeysTable($device), + $this->buildServicesTable($device), $timeline, ), array( @@ -116,7 +129,8 @@ $table = id(new AlmanacInterfaceTableView()) ->setUser($viewer) ->setInterfaces($interfaces) - ->setHandles($handles); + ->setHandles($handles) + ->setCanEdit($can_edit); $header = id(new PHUIHeaderView()) ->setHeader(pht('Device Interfaces')) @@ -199,4 +213,52 @@ } + private function buildServicesTable(AlmanacDevice $device) { + + // NOTE: We're loading all services so we can show hidden, locked services. + // In general, we let you know about all the things the device is bound to, + // even if you don't have permission to see their details. This is similar + // to exposing the existence of edges in other applications, with the + // addition of always letting you see that locks exist. + + $services = id(new AlmanacServiceQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withDevicePHIDs(array($device->getPHID())) + ->execute(); + + $handles = $this->loadViewerHandles(mpull($services, 'getPHID')); + + $icon_lock = id(new PHUIIconView()) + ->setIconFont('fa-lock'); + + $rows = array(); + foreach ($services as $service) { + $handle = $handles[$service->getPHID()]; + $rows[] = array( + ($service->getIsLocked() + ? $icon_lock + : null), + $handle->renderLink(), + ); + } + + $table = id(new AphrontTableView($rows)) + ->setNoDataString(pht('No services are bound to this device.')) + ->setHeaders( + array( + null, + pht('Service'), + )) + ->setColumnClasses( + array( + null, + 'wide pri', + )); + + return id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Bound Services')) + ->appendChild($table); + } + + } 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 @@ -35,6 +35,17 @@ ->setHeader($header) ->addPropertyList($property_list); + $messages = $service->getServiceType()->getStatusMessages($service); + if ($messages) { + $box->setFormErrors($messages); + } + + if ($service->getIsLocked()) { + $this->addLockMessage( + $box, + pht('This service is locked, and can not be edited.')); + } + $bindings = $this->buildBindingList($service); $crumbs = $this->buildApplicationCrumbs(); diff --git a/src/applications/almanac/editor/AlmanacServiceEditor.php b/src/applications/almanac/editor/AlmanacServiceEditor.php --- a/src/applications/almanac/editor/AlmanacServiceEditor.php +++ b/src/applications/almanac/editor/AlmanacServiceEditor.php @@ -15,6 +15,8 @@ $types = parent::getTransactionTypes(); $types[] = AlmanacServiceTransaction::TYPE_NAME; + $types[] = AlmanacServiceTransaction::TYPE_LOCK; + $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; @@ -27,6 +29,8 @@ switch ($xaction->getTransactionType()) { case AlmanacServiceTransaction::TYPE_NAME: return $object->getName(); + case AlmanacServiceTransaction::TYPE_LOCK: + return (bool)$object->getIsLocked(); } return parent::getCustomTransactionOldValue($object, $xaction); @@ -39,6 +43,8 @@ switch ($xaction->getTransactionType()) { case AlmanacServiceTransaction::TYPE_NAME: return $xaction->getNewValue(); + case AlmanacServiceTransaction::TYPE_LOCK: + return (bool)$xaction->getNewValue(); } return parent::getCustomTransactionNewValue($object, $xaction); @@ -52,6 +58,9 @@ case AlmanacServiceTransaction::TYPE_NAME: $object->setName($xaction->getNewValue()); return; + case AlmanacServiceTransaction::TYPE_LOCK: + $object->setIsLocked((int)$xaction->getNewValue()); + return; case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_EDIT_POLICY: case PhabricatorTransactions::TYPE_EDGE: @@ -71,6 +80,23 @@ case PhabricatorTransactions::TYPE_EDIT_POLICY: case PhabricatorTransactions::TYPE_EDGE: return; + case AlmanacServiceTransaction::TYPE_LOCK: + $service = id(new AlmanacServiceQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPHIDs(array($object->getPHID())) + ->needBindings(true) + ->executeOne(); + + $devices = array(); + foreach ($service->getBindings() as $binding) { + $device = $binding->getInterface()->getDevice(); + $devices[$device->getPHID()] = $device; + } + + foreach ($devices as $device) { + $device->rebuildDeviceLocks(); + } + return; } return parent::applyCustomExternalTransaction($object, $xaction); diff --git a/src/applications/almanac/management/AlmanacManagementLockWorkflow.php b/src/applications/almanac/management/AlmanacManagementLockWorkflow.php new file mode 100644 --- /dev/null +++ b/src/applications/almanac/management/AlmanacManagementLockWorkflow.php @@ -0,0 +1,49 @@ +setName('lock') + ->setSynopsis(pht('Lock a service to prevent it from being edited.')) + ->setArguments( + array( + array( + 'name' => 'services', + 'wildcard' => true, + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $console = PhutilConsole::getConsole(); + + $services = $this->loadServices($args->getArg('services')); + if (!$services) { + throw new PhutilArgumentUsageException( + pht('Specify at least one service to lock.')); + } + + foreach ($services as $service) { + if ($service->getIsLocked()) { + throw new PhutilArgumentUsageException( + pht( + 'Service "%s" is already locked!', + $service->getName())); + } + } + + foreach ($services as $service) { + $this->updateServiceLock($service, true); + + $console->writeOut( + "** %s ** %s\n", + pht('LOCKED'), + pht('Service "%s" was locked.', $service->getName())); + } + + return 0; + } + +} diff --git a/src/applications/almanac/management/AlmanacManagementUnlockWorkflow.php b/src/applications/almanac/management/AlmanacManagementUnlockWorkflow.php new file mode 100644 --- /dev/null +++ b/src/applications/almanac/management/AlmanacManagementUnlockWorkflow.php @@ -0,0 +1,49 @@ +setName('unlock') + ->setSynopsis(pht('Unlock a service to allow it to be edited.')) + ->setArguments( + array( + array( + 'name' => 'services', + 'wildcard' => true, + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $console = PhutilConsole::getConsole(); + + $services = $this->loadServices($args->getArg('services')); + if (!$services) { + throw new PhutilArgumentUsageException( + pht('Specify at least one service to unlock.')); + } + + foreach ($services as $service) { + if (!$service->getIsLocked()) { + throw new PhutilArgumentUsageException( + pht( + 'Service "%s" is not locked!', + $service->getName())); + } + } + + foreach ($services as $service) { + $this->updateServiceLock($service, false); + + $console->writeOut( + "** %s ** %s\n", + pht('UNLOCKED'), + pht('Service "%s" was unlocked.', $service->getName())); + } + + return 0; + } + +} diff --git a/src/applications/almanac/management/AlmanacManagementWorkflow.php b/src/applications/almanac/management/AlmanacManagementWorkflow.php --- a/src/applications/almanac/management/AlmanacManagementWorkflow.php +++ b/src/applications/almanac/management/AlmanacManagementWorkflow.php @@ -1,4 +1,46 @@ setViewer($this->getViewer()) + ->withNames($names) + ->execute(); + + $services = mpull($services, null, 'getName'); + foreach ($names as $name) { + if (empty($services[$name])) { + throw new PhutilArgumentUsageException( + pht( + 'Service "%s" does not exist or could not be loaded!', + $name)); + } + } + + return $services; + } + + protected function updateServiceLock(AlmanacService $service, $lock) { + $almanac_phid = id(new PhabricatorAlmanacApplication())->getPHID(); + + $xaction = id(new AlmanacServiceTransaction()) + ->setTransactionType(AlmanacServiceTransaction::TYPE_LOCK) + ->setNewValue((int)$lock); + + $editor = id(new AlmanacServiceEditor()) + ->setActor($this->getViewer()) + ->setActingAsPHID($almanac_phid) + ->setContentSource(PhabricatorContentSource::newConsoleSource()) + ->setContinueOnMissingFields(true); + + $editor->applyTransactions($service, array($xaction)); + } + +} diff --git a/src/applications/almanac/query/AlmanacServiceQuery.php b/src/applications/almanac/query/AlmanacServiceQuery.php --- a/src/applications/almanac/query/AlmanacServiceQuery.php +++ b/src/applications/almanac/query/AlmanacServiceQuery.php @@ -7,6 +7,9 @@ private $phids; private $names; private $serviceClasses; + private $devicePHIDs; + private $locked; + private $needBindings; public function withIDs(array $ids) { @@ -29,6 +32,16 @@ return $this; } + public function withDevicePHIDs(array $phids) { + $this->devicePHIDs = $phids; + return $this; + } + + public function withLocked($locked) { + $this->locked = $locked; + return $this; + } + public function needBindings($need_bindings) { $this->needBindings = $need_bindings; return $this; @@ -40,8 +53,9 @@ $data = queryfx_all( $conn_r, - 'SELECT * FROM %T %Q %Q %Q', + 'SELECT service.* FROM %T service %Q %Q %Q %Q', $table->getTableName(), + $this->buildJoinClause($conn_r), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); @@ -49,20 +63,33 @@ return $table->loadAllFromArray($data); } + protected function buildJoinClause($conn_r) { + $joins = array(); + + if ($this->devicePHIDs !== null) { + $joins[] = qsprintf( + $conn_r, + 'JOIN %T binding ON service.phid = binding.servicePHID', + id(new AlmanacBinding())->getTableName()); + } + + return implode(' ', $joins); + } + protected function buildWhereClause($conn_r) { $where = array(); if ($this->ids !== null) { $where[] = qsprintf( $conn_r, - 'id IN (%Ld)', + 'service.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn_r, - 'phid IN (%Ls)', + 'service.phid IN (%Ls)', $this->phids); } @@ -74,17 +101,31 @@ $where[] = qsprintf( $conn_r, - 'nameIndex IN (%Ls)', + 'service.nameIndex IN (%Ls)', $hashes); } if ($this->serviceClasses !== null) { $where[] = qsprintf( $conn_r, - 'serviceClass IN (%Ls)', + 'service.serviceClass IN (%Ls)', $this->serviceClasses); } + if ($this->devicePHIDs !== null) { + $where[] = qsprintf( + $conn_r, + 'binding.devicePHID IN (%Ls)', + $this->devicePHIDs); + } + + if ($this->locked !== null) { + $where[] = qsprintf( + $conn_r, + 'service.isLocked = %d', + (int)$this->locked); + } + $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); diff --git a/src/applications/almanac/query/AlmanacServiceSearchEngine.php b/src/applications/almanac/query/AlmanacServiceSearchEngine.php --- a/src/applications/almanac/query/AlmanacServiceSearchEngine.php +++ b/src/applications/almanac/query/AlmanacServiceSearchEngine.php @@ -78,6 +78,15 @@ $service->getServiceType()->getServiceTypeIcon(), $service->getServiceType()->getServiceTypeShortName()); + if ($service->getIsLocked() || + $service->getServiceType()->isClusterServiceType()) { + if ($service->getIsLocked()) { + $item->addIcon('fa-lock', pht('Locked')); + } else { + $item->addIcon('fa-unlock-alt red', pht('Unlocked')); + } + } + $list->addItem($item); } diff --git a/src/applications/almanac/servicetype/AlmanacClusterServiceType.php b/src/applications/almanac/servicetype/AlmanacClusterServiceType.php --- a/src/applications/almanac/servicetype/AlmanacClusterServiceType.php +++ b/src/applications/almanac/servicetype/AlmanacClusterServiceType.php @@ -11,4 +11,28 @@ return 'fa-sitemap'; } + public function getStatusMessages(AlmanacService $service) { + $messages = parent::getStatusMessages($service); + + if (!$service->getIsLocked()) { + $doc_href = PhabricatorEnv::getDoclink( + 'User Guide: Phabricator Clusters'); + + $doc_link = phutil_tag( + 'a', + array( + 'href' => $doc_href, + 'target' => '_blank', + ), + pht('Learn More')); + + $messages[] = pht( + 'This is an unlocked cluster service. After you finish editing '. + 'it, you should lock it. %s.', + $doc_link); + } + + return $messages; + } + } diff --git a/src/applications/almanac/servicetype/AlmanacServiceType.php b/src/applications/almanac/servicetype/AlmanacServiceType.php --- a/src/applications/almanac/servicetype/AlmanacServiceType.php +++ b/src/applications/almanac/servicetype/AlmanacServiceType.php @@ -55,6 +55,10 @@ return array(); } + public function getStatusMessages(AlmanacService $service) { + return array(); + } + /** * List all available service type implementations. * 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 @@ -143,12 +143,21 @@ } public function describeAutomaticCapability($capability) { - return array( + $notes = array( pht('A binding inherits the policies of its service.'), pht( 'To view a binding, you must also be able to view its device and '. 'interface.'), ); + + if ($capability === PhabricatorPolicyCapability::CAN_EDIT) { + if ($this->getService()->getIsLocked()) { + $notes[] = pht( + 'The service for this binding is locked, so it can not be edited.'); + } + } + + return $notes; } diff --git a/src/applications/almanac/storage/AlmanacDevice.php b/src/applications/almanac/storage/AlmanacDevice.php --- a/src/applications/almanac/storage/AlmanacDevice.php +++ b/src/applications/almanac/storage/AlmanacDevice.php @@ -15,6 +15,7 @@ protected $mailKey; protected $viewPolicy; protected $editPolicy; + protected $isLocked; private $customFields = self::ATTACHABLE; private $almanacProperties = self::ATTACHABLE; @@ -23,7 +24,8 @@ return id(new AlmanacDevice()) ->setViewPolicy(PhabricatorPolicies::POLICY_USER) ->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN) - ->attachAlmanacProperties(array()); + ->attachAlmanacProperties(array()) + ->setIsLocked(0); } public function getConfiguration() { @@ -33,6 +35,7 @@ 'name' => 'text128', 'nameIndex' => 'bytes12', 'mailKey' => 'bytes20', + 'isLocked' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_name' => array( @@ -67,6 +70,37 @@ } + /** + * Find locked services which are bound to this device, updating the device + * lock flag if necessary. + * + * @return list List of locking service PHIDs. + */ + public function rebuildDeviceLocks() { + $services = id(new AlmanacServiceQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withDevicePHIDs(array($this->getPHID())) + ->withLocked(true) + ->execute(); + + $locked = (bool)count($services); + + if ($locked != $this->getIsLocked()) { + $this->setIsLocked((int)$locked); + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + queryfx( + $this->establishConnection('w'), + 'UPDATE %T SET isLocked = %d WHERE id = %d', + $this->getTableName(), + $this->getIsLocked(), + $this->getID()); + unset($unguarded); + } + + return $this; + } + + /* -( AlmanacPropertyInterface )------------------------------------------- */ @@ -117,7 +151,11 @@ case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: - return $this->getEditPolicy(); + if ($this->getIsLocked()) { + return PhabricatorPolicies::POLICY_NOONE; + } else { + return $this->getEditPolicy(); + } } } @@ -126,6 +164,14 @@ } public function describeAutomaticCapability($capability) { + if ($capability === PhabricatorPolicyCapability::CAN_EDIT) { + if ($this->getIsLocked()) { + return pht( + 'This device is bound to a locked service, so it can not '. + 'be edited.'); + } + } + return null; } 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 @@ -92,12 +92,21 @@ } public function describeAutomaticCapability($capability) { - return array( + $notes = array( pht('An interface inherits the policies of the device it belongs to.'), pht( 'You must be able to view the network an interface resides on to '. 'view the interface.'), ); + + if ($capability === PhabricatorPolicyCapability::CAN_EDIT) { + if ($this->getDevice()->getIsLocked()) { + $notes[] = pht( + 'The device for this interface is locked, so it can not be edited.'); + } + } + + return $notes; } } diff --git a/src/applications/almanac/storage/AlmanacService.php b/src/applications/almanac/storage/AlmanacService.php --- a/src/applications/almanac/storage/AlmanacService.php +++ b/src/applications/almanac/storage/AlmanacService.php @@ -15,6 +15,7 @@ protected $viewPolicy; protected $editPolicy; protected $serviceClass; + protected $isLocked; private $customFields = self::ATTACHABLE; private $almanacProperties = self::ATTACHABLE; @@ -25,7 +26,8 @@ return id(new AlmanacService()) ->setViewPolicy(PhabricatorPolicies::POLICY_USER) ->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN) - ->attachAlmanacProperties(array()); + ->attachAlmanacProperties(array()) + ->setIsLocked(0); } public function getConfiguration() { @@ -36,6 +38,7 @@ 'nameIndex' => 'bytes12', 'mailKey' => 'bytes20', 'serviceClass' => 'text64', + 'isLocked' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_name' => array( @@ -141,7 +144,11 @@ case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: - return $this->getEditPolicy(); + if ($this->getIsLocked()) { + return PhabricatorPolicies::POLICY_NOONE; + } else { + return $this->getEditPolicy(); + } } } @@ -150,6 +157,14 @@ } public function describeAutomaticCapability($capability) { + switch ($capability) { + case PhabricatorPolicyCapability::CAN_EDIT: + if ($this->getIsLocked()) { + return pht('This service is locked and can not be edited.'); + } + break; + } + return null; } diff --git a/src/applications/almanac/storage/AlmanacServiceTransaction.php b/src/applications/almanac/storage/AlmanacServiceTransaction.php --- a/src/applications/almanac/storage/AlmanacServiceTransaction.php +++ b/src/applications/almanac/storage/AlmanacServiceTransaction.php @@ -4,6 +4,7 @@ extends PhabricatorApplicationTransaction { const TYPE_NAME = 'almanac:service:name'; + const TYPE_LOCK = 'almanac:service:lock'; public function getApplicationName() { return 'almanac'; @@ -37,6 +38,17 @@ $new); } break; + case self::TYPE_LOCK: + if ($new) { + return pht( + '%s locked this service.', + $this->renderHandleLink($author_phid)); + } else { + return pht( + '%s unlocked this service.', + $this->renderHandleLink($author_phid)); + } + break; } return parent::getTitle(); 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 @@ -4,6 +4,7 @@ private $interfaces; private $handles; + private $canEdit; public function setHandles(array $handles) { $this->handles = $handles; @@ -23,11 +24,26 @@ return $this->interfaces; } + public function setCanEdit($can_edit) { + $this->canEdit = $can_edit; + return $this; + } + + public function getCanEdit() { + return $this->canEdit; + } + public function render() { $interfaces = $this->getInterfaces(); $handles = $this->getHandles(); $viewer = $this->getUser(); + if ($this->getCanEdit()) { + $button_class = 'small grey button'; + } else { + $button_class = 'small grey button disabled'; + } + $rows = array(); foreach ($interfaces as $interface) { $rows[] = array( @@ -38,7 +54,7 @@ phutil_tag( 'a', array( - 'class' => 'small grey button', + 'class' => $button_class, 'href' => '/almanac/interface/edit/'.$interface->getID().'/', ), pht('Edit')), diff --git a/src/docs/user/configuration/cluster.diviner b/src/docs/user/configuration/cluster.diviner new file mode 100644 --- /dev/null +++ b/src/docs/user/configuration/cluster.diviner @@ -0,0 +1,31 @@ +@title User Guide: Phabricator Clusters +@group config + +Guide on scaling Phabricator across multiple machines, for large installs. + +Overview +======== + +IMPORTANT: Phabricator clustering is in its infancy and does not work at all +yet. This document is mostly a placeholder. + +Locking Services +================ + +Because cluster configuration is defined in Phabricator itself, an attacker +who compromises an account that can edit the cluster definition has significant +power. For example, the attacker might be able to configure Phabricator to +replicate the database to a server they control. + +To mitigate this attack, services in Almanac can be locked to prevent them +from being edited from the web UI. An attacker would then need significantly +greater access (to the CLI, or directly to the database) in order to change +the cluster configuration. + +You should normally keep cluster services in a locked state, and unlock them +only to edit them. Once you're finished making changes, lock the service again. +The web UI will warn you when you're viewing an unlocked cluster service, as +a reminder that you should lock it again once you're finished editing. + +For details on how to lock and unlock a service, see +@{article:Almanac User Guide}. diff --git a/src/docs/user/userguide/almanac.diviner b/src/docs/user/userguide/almanac.diviner new file mode 100644 --- /dev/null +++ b/src/docs/user/userguide/almanac.diviner @@ -0,0 +1,40 @@ +@title Almanac User Guide +@group userguide + +Using Almanac to manage services. + += Overview = + +IMPORTANT: Almanac is a prototype application. See +@{article:User Guide: Prototype Applications}. + +Locking and Unlocking Services +============================== + +Services can be locked to prevent edits from the web UI. This primarily hardens +Almanac against attacks involving account compromise. Notably, locking cluster +services prevents an attacker from modifying the Phabricator cluster definition. +For more details on this scenario, see +@{article:User Guide: Phabricator Clusters}. + +Beyond hardening cluster definitions, you might also want to lock a service to +prevent accidental edits. + +To lock a service, run: + + phabricator/ $ ./bin/almanac lock + +To unlock a service later, run: + + phabricator/ $ ./bin/almanac unlock + +Locking a service also locks all of the service's bindings and properties, as +well as the devices connected to the service. Generally, no part of the +service definition can be modified while it is locked. + +Devices (and their properties) will remain locked as long as they are bound to +at least one locked service. To edit a device, you'll need to unlock all the +services it is bound to. + +Locked services and devices will show that they are locked in the web UI, and +editing options will be unavailable.