Page MenuHomePhabricator

D11006.diff
No OneTemporary

D11006.diff

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 @@
+<?php
+
+final class AlmanacManagementLockWorkflow
+ extends AlmanacManagementWorkflow {
+
+ public function didConstruct() {
+ $this
+ ->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(
+ "**<bg:green> %s </bg>** %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 @@
+<?php
+
+final class AlmanacManagementUnlockWorkflow
+ extends AlmanacManagementWorkflow {
+
+ public function didConstruct() {
+ $this
+ ->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(
+ "**<bg:green> %s </bg>** %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 @@
<?php
abstract class AlmanacManagementWorkflow
- extends PhabricatorManagementWorkflow {}
+ extends PhabricatorManagementWorkflow {
+
+
+ protected function loadServices(array $names) {
+ if (!$names) {
+ return array();
+ }
+
+ $services = id(new AlmanacServiceQuery())
+ ->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<phid> 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 <service>
+
+To unlock a service later, run:
+
+ phabricator/ $ ./bin/almanac unlock <service>
+
+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.

File Metadata

Mime Type
text/plain
Expires
Mon, Mar 10, 6:28 AM (1 w, 5 d ago)
Storage Engine
amazon-s3
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
phabricator/secure/d5/hl/htknbprrhi2kirgq
Default Alt Text
D11006.diff (31 KB)

Event Timeline