Allow Almanac services to be locked
Summary: Fixes T6741. This allows Almanac services to be locked from the CLI. Locked services (and their bindings, interfaces and devices) can not be edited. This serves two similar use cases: - For normal installs, you can protect cluster configuration from an attacker who compromises an account (or generally harden services which are intended to be difficult to edit). - For Phacility, we can lock externally-managed instance cluster configuration without having to pull any spooky tricks. Test Plan: - Locked and unlocked services. - Verified locking a service locks connected properties, bindings, binding properties, interfaces, devices, and device properties. Reviewers: btrahan Reviewed By: btrahan Subscribers: epriestley Maniphest Tasks: T6741 Differential Revision: https://secure.phabricator.com/D11006
This commit is contained in:
parent
cd6f67ef95
commit
d2df3064bc
2
resources/sql/autopatches/20141217.almanacdevicelock.sql
Normal file
2
resources/sql/autopatches/20141217.almanacdevicelock.sql
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE {$NAMESPACE}_almanac.almanac_device
|
||||||
|
ADD isLocked BOOL NOT NULL;
|
2
resources/sql/autopatches/20141217.almanaclock.sql
Normal file
2
resources/sql/autopatches/20141217.almanaclock.sql
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE {$NAMESPACE}_almanac.almanac_service
|
||||||
|
ADD isLocked BOOL NOT NULL;
|
|
@ -49,7 +49,9 @@ phutil_register_library_map(array(
|
||||||
'AlmanacInterfacePHIDType' => 'applications/almanac/phid/AlmanacInterfacePHIDType.php',
|
'AlmanacInterfacePHIDType' => 'applications/almanac/phid/AlmanacInterfacePHIDType.php',
|
||||||
'AlmanacInterfaceQuery' => 'applications/almanac/query/AlmanacInterfaceQuery.php',
|
'AlmanacInterfaceQuery' => 'applications/almanac/query/AlmanacInterfaceQuery.php',
|
||||||
'AlmanacInterfaceTableView' => 'applications/almanac/view/AlmanacInterfaceTableView.php',
|
'AlmanacInterfaceTableView' => 'applications/almanac/view/AlmanacInterfaceTableView.php',
|
||||||
|
'AlmanacManagementLockWorkflow' => 'applications/almanac/management/AlmanacManagementLockWorkflow.php',
|
||||||
'AlmanacManagementTrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementTrustKeyWorkflow.php',
|
'AlmanacManagementTrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementTrustKeyWorkflow.php',
|
||||||
|
'AlmanacManagementUnlockWorkflow' => 'applications/almanac/management/AlmanacManagementUnlockWorkflow.php',
|
||||||
'AlmanacManagementUntrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementUntrustKeyWorkflow.php',
|
'AlmanacManagementUntrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementUntrustKeyWorkflow.php',
|
||||||
'AlmanacManagementWorkflow' => 'applications/almanac/management/AlmanacManagementWorkflow.php',
|
'AlmanacManagementWorkflow' => 'applications/almanac/management/AlmanacManagementWorkflow.php',
|
||||||
'AlmanacNames' => 'applications/almanac/util/AlmanacNames.php',
|
'AlmanacNames' => 'applications/almanac/util/AlmanacNames.php',
|
||||||
|
@ -3068,7 +3070,9 @@ phutil_register_library_map(array(
|
||||||
'AlmanacInterfacePHIDType' => 'PhabricatorPHIDType',
|
'AlmanacInterfacePHIDType' => 'PhabricatorPHIDType',
|
||||||
'AlmanacInterfaceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
|
'AlmanacInterfaceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
|
||||||
'AlmanacInterfaceTableView' => 'AphrontView',
|
'AlmanacInterfaceTableView' => 'AphrontView',
|
||||||
|
'AlmanacManagementLockWorkflow' => 'AlmanacManagementWorkflow',
|
||||||
'AlmanacManagementTrustKeyWorkflow' => 'AlmanacManagementWorkflow',
|
'AlmanacManagementTrustKeyWorkflow' => 'AlmanacManagementWorkflow',
|
||||||
|
'AlmanacManagementUnlockWorkflow' => 'AlmanacManagementWorkflow',
|
||||||
'AlmanacManagementUntrustKeyWorkflow' => 'AlmanacManagementWorkflow',
|
'AlmanacManagementUntrustKeyWorkflow' => 'AlmanacManagementWorkflow',
|
||||||
'AlmanacManagementWorkflow' => 'PhabricatorManagementWorkflow',
|
'AlmanacManagementWorkflow' => 'PhabricatorManagementWorkflow',
|
||||||
'AlmanacNames' => 'Phobject',
|
'AlmanacNames' => 'Phobject',
|
||||||
|
|
|
@ -26,6 +26,10 @@ final class PhabricatorAlmanacApplication extends PhabricatorApplication {
|
||||||
return self::GROUP_UTILITIES;
|
return self::GROUP_UTILITIES;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getHelpURI() {
|
||||||
|
return PhabricatorEnv::getDoclink('Almanac User Guide');
|
||||||
|
}
|
||||||
|
|
||||||
public function isPrototype() {
|
public function isPrototype() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,14 @@ final class AlmanacBindingViewController
|
||||||
->setHeader($header)
|
->setHeader($header)
|
||||||
->addPropertyList($property_list);
|
->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 = $this->buildApplicationCrumbs();
|
||||||
$crumbs->addTextCrumb($service->getName(), $service_uri);
|
$crumbs->addTextCrumb($service->getName(), $service_uri);
|
||||||
$crumbs->addTextCrumb($title);
|
$crumbs->addTextCrumb($title);
|
||||||
|
|
|
@ -179,4 +179,23 @@ abstract class AlmanacController
|
||||||
->appendChild($table);
|
->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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,10 @@ final class AlmanacDeviceViewController
|
||||||
return new Aphront404Response();
|
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());
|
$title = pht('Device %s', $device->getName());
|
||||||
|
|
||||||
$property_list = $this->buildPropertyList($device);
|
$property_list = $this->buildPropertyList($device);
|
||||||
|
@ -35,6 +39,14 @@ final class AlmanacDeviceViewController
|
||||||
->setHeader($header)
|
->setHeader($header)
|
||||||
->addPropertyList($property_list);
|
->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);
|
$interfaces = $this->buildInterfaceList($device);
|
||||||
|
|
||||||
$crumbs = $this->buildApplicationCrumbs();
|
$crumbs = $this->buildApplicationCrumbs();
|
||||||
|
@ -52,6 +64,7 @@ final class AlmanacDeviceViewController
|
||||||
$interfaces,
|
$interfaces,
|
||||||
$this->buildAlmanacPropertiesTable($device),
|
$this->buildAlmanacPropertiesTable($device),
|
||||||
$this->buildSSHKeysTable($device),
|
$this->buildSSHKeysTable($device),
|
||||||
|
$this->buildServicesTable($device),
|
||||||
$timeline,
|
$timeline,
|
||||||
),
|
),
|
||||||
array(
|
array(
|
||||||
|
@ -116,7 +129,8 @@ final class AlmanacDeviceViewController
|
||||||
$table = id(new AlmanacInterfaceTableView())
|
$table = id(new AlmanacInterfaceTableView())
|
||||||
->setUser($viewer)
|
->setUser($viewer)
|
||||||
->setInterfaces($interfaces)
|
->setInterfaces($interfaces)
|
||||||
->setHandles($handles);
|
->setHandles($handles)
|
||||||
|
->setCanEdit($can_edit);
|
||||||
|
|
||||||
$header = id(new PHUIHeaderView())
|
$header = id(new PHUIHeaderView())
|
||||||
->setHeader(pht('Device Interfaces'))
|
->setHeader(pht('Device Interfaces'))
|
||||||
|
@ -199,4 +213,52 @@ final class AlmanacDeviceViewController
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,17 @@ final class AlmanacServiceViewController
|
||||||
->setHeader($header)
|
->setHeader($header)
|
||||||
->addPropertyList($property_list);
|
->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);
|
$bindings = $this->buildBindingList($service);
|
||||||
|
|
||||||
$crumbs = $this->buildApplicationCrumbs();
|
$crumbs = $this->buildApplicationCrumbs();
|
||||||
|
|
|
@ -15,6 +15,8 @@ final class AlmanacServiceEditor
|
||||||
$types = parent::getTransactionTypes();
|
$types = parent::getTransactionTypes();
|
||||||
|
|
||||||
$types[] = AlmanacServiceTransaction::TYPE_NAME;
|
$types[] = AlmanacServiceTransaction::TYPE_NAME;
|
||||||
|
$types[] = AlmanacServiceTransaction::TYPE_LOCK;
|
||||||
|
|
||||||
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
|
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
|
||||||
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
|
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
|
||||||
|
|
||||||
|
@ -27,6 +29,8 @@ final class AlmanacServiceEditor
|
||||||
switch ($xaction->getTransactionType()) {
|
switch ($xaction->getTransactionType()) {
|
||||||
case AlmanacServiceTransaction::TYPE_NAME:
|
case AlmanacServiceTransaction::TYPE_NAME:
|
||||||
return $object->getName();
|
return $object->getName();
|
||||||
|
case AlmanacServiceTransaction::TYPE_LOCK:
|
||||||
|
return (bool)$object->getIsLocked();
|
||||||
}
|
}
|
||||||
|
|
||||||
return parent::getCustomTransactionOldValue($object, $xaction);
|
return parent::getCustomTransactionOldValue($object, $xaction);
|
||||||
|
@ -39,6 +43,8 @@ final class AlmanacServiceEditor
|
||||||
switch ($xaction->getTransactionType()) {
|
switch ($xaction->getTransactionType()) {
|
||||||
case AlmanacServiceTransaction::TYPE_NAME:
|
case AlmanacServiceTransaction::TYPE_NAME:
|
||||||
return $xaction->getNewValue();
|
return $xaction->getNewValue();
|
||||||
|
case AlmanacServiceTransaction::TYPE_LOCK:
|
||||||
|
return (bool)$xaction->getNewValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
return parent::getCustomTransactionNewValue($object, $xaction);
|
return parent::getCustomTransactionNewValue($object, $xaction);
|
||||||
|
@ -52,6 +58,9 @@ final class AlmanacServiceEditor
|
||||||
case AlmanacServiceTransaction::TYPE_NAME:
|
case AlmanacServiceTransaction::TYPE_NAME:
|
||||||
$object->setName($xaction->getNewValue());
|
$object->setName($xaction->getNewValue());
|
||||||
return;
|
return;
|
||||||
|
case AlmanacServiceTransaction::TYPE_LOCK:
|
||||||
|
$object->setIsLocked((int)$xaction->getNewValue());
|
||||||
|
return;
|
||||||
case PhabricatorTransactions::TYPE_VIEW_POLICY:
|
case PhabricatorTransactions::TYPE_VIEW_POLICY:
|
||||||
case PhabricatorTransactions::TYPE_EDIT_POLICY:
|
case PhabricatorTransactions::TYPE_EDIT_POLICY:
|
||||||
case PhabricatorTransactions::TYPE_EDGE:
|
case PhabricatorTransactions::TYPE_EDGE:
|
||||||
|
@ -71,6 +80,23 @@ final class AlmanacServiceEditor
|
||||||
case PhabricatorTransactions::TYPE_EDIT_POLICY:
|
case PhabricatorTransactions::TYPE_EDIT_POLICY:
|
||||||
case PhabricatorTransactions::TYPE_EDGE:
|
case PhabricatorTransactions::TYPE_EDGE:
|
||||||
return;
|
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);
|
return parent::applyCustomExternalTransaction($object, $xaction);
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,4 +1,46 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
abstract class AlmanacManagementWorkflow
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -7,6 +7,9 @@ final class AlmanacServiceQuery
|
||||||
private $phids;
|
private $phids;
|
||||||
private $names;
|
private $names;
|
||||||
private $serviceClasses;
|
private $serviceClasses;
|
||||||
|
private $devicePHIDs;
|
||||||
|
private $locked;
|
||||||
|
|
||||||
private $needBindings;
|
private $needBindings;
|
||||||
|
|
||||||
public function withIDs(array $ids) {
|
public function withIDs(array $ids) {
|
||||||
|
@ -29,6 +32,16 @@ final class AlmanacServiceQuery
|
||||||
return $this;
|
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) {
|
public function needBindings($need_bindings) {
|
||||||
$this->needBindings = $need_bindings;
|
$this->needBindings = $need_bindings;
|
||||||
return $this;
|
return $this;
|
||||||
|
@ -40,8 +53,9 @@ final class AlmanacServiceQuery
|
||||||
|
|
||||||
$data = queryfx_all(
|
$data = queryfx_all(
|
||||||
$conn_r,
|
$conn_r,
|
||||||
'SELECT * FROM %T %Q %Q %Q',
|
'SELECT service.* FROM %T service %Q %Q %Q %Q',
|
||||||
$table->getTableName(),
|
$table->getTableName(),
|
||||||
|
$this->buildJoinClause($conn_r),
|
||||||
$this->buildWhereClause($conn_r),
|
$this->buildWhereClause($conn_r),
|
||||||
$this->buildOrderClause($conn_r),
|
$this->buildOrderClause($conn_r),
|
||||||
$this->buildLimitClause($conn_r));
|
$this->buildLimitClause($conn_r));
|
||||||
|
@ -49,20 +63,33 @@ final class AlmanacServiceQuery
|
||||||
return $table->loadAllFromArray($data);
|
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) {
|
protected function buildWhereClause($conn_r) {
|
||||||
$where = array();
|
$where = array();
|
||||||
|
|
||||||
if ($this->ids !== null) {
|
if ($this->ids !== null) {
|
||||||
$where[] = qsprintf(
|
$where[] = qsprintf(
|
||||||
$conn_r,
|
$conn_r,
|
||||||
'id IN (%Ld)',
|
'service.id IN (%Ld)',
|
||||||
$this->ids);
|
$this->ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->phids !== null) {
|
if ($this->phids !== null) {
|
||||||
$where[] = qsprintf(
|
$where[] = qsprintf(
|
||||||
$conn_r,
|
$conn_r,
|
||||||
'phid IN (%Ls)',
|
'service.phid IN (%Ls)',
|
||||||
$this->phids);
|
$this->phids);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,17 +101,31 @@ final class AlmanacServiceQuery
|
||||||
|
|
||||||
$where[] = qsprintf(
|
$where[] = qsprintf(
|
||||||
$conn_r,
|
$conn_r,
|
||||||
'nameIndex IN (%Ls)',
|
'service.nameIndex IN (%Ls)',
|
||||||
$hashes);
|
$hashes);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->serviceClasses !== null) {
|
if ($this->serviceClasses !== null) {
|
||||||
$where[] = qsprintf(
|
$where[] = qsprintf(
|
||||||
$conn_r,
|
$conn_r,
|
||||||
'serviceClass IN (%Ls)',
|
'service.serviceClass IN (%Ls)',
|
||||||
$this->serviceClasses);
|
$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);
|
$where[] = $this->buildPagingClause($conn_r);
|
||||||
|
|
||||||
return $this->formatWhereClause($where);
|
return $this->formatWhereClause($where);
|
||||||
|
|
|
@ -78,6 +78,15 @@ final class AlmanacServiceSearchEngine
|
||||||
$service->getServiceType()->getServiceTypeIcon(),
|
$service->getServiceType()->getServiceTypeIcon(),
|
||||||
$service->getServiceType()->getServiceTypeShortName());
|
$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);
|
$list->addItem($item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,4 +11,28 @@ abstract class AlmanacClusterServiceType
|
||||||
return 'fa-sitemap';
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,6 +55,10 @@ abstract class AlmanacServiceType extends Phobject {
|
||||||
return array();
|
return array();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getStatusMessages(AlmanacService $service) {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List all available service type implementations.
|
* List all available service type implementations.
|
||||||
*
|
*
|
||||||
|
|
|
@ -143,12 +143,21 @@ final class AlmanacBinding
|
||||||
}
|
}
|
||||||
|
|
||||||
public function describeAutomaticCapability($capability) {
|
public function describeAutomaticCapability($capability) {
|
||||||
return array(
|
$notes = array(
|
||||||
pht('A binding inherits the policies of its service.'),
|
pht('A binding inherits the policies of its service.'),
|
||||||
pht(
|
pht(
|
||||||
'To view a binding, you must also be able to view its device and '.
|
'To view a binding, you must also be able to view its device and '.
|
||||||
'interface.'),
|
'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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ final class AlmanacDevice
|
||||||
protected $mailKey;
|
protected $mailKey;
|
||||||
protected $viewPolicy;
|
protected $viewPolicy;
|
||||||
protected $editPolicy;
|
protected $editPolicy;
|
||||||
|
protected $isLocked;
|
||||||
|
|
||||||
private $customFields = self::ATTACHABLE;
|
private $customFields = self::ATTACHABLE;
|
||||||
private $almanacProperties = self::ATTACHABLE;
|
private $almanacProperties = self::ATTACHABLE;
|
||||||
|
@ -23,7 +24,8 @@ final class AlmanacDevice
|
||||||
return id(new AlmanacDevice())
|
return id(new AlmanacDevice())
|
||||||
->setViewPolicy(PhabricatorPolicies::POLICY_USER)
|
->setViewPolicy(PhabricatorPolicies::POLICY_USER)
|
||||||
->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN)
|
->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN)
|
||||||
->attachAlmanacProperties(array());
|
->attachAlmanacProperties(array())
|
||||||
|
->setIsLocked(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getConfiguration() {
|
public function getConfiguration() {
|
||||||
|
@ -33,6 +35,7 @@ final class AlmanacDevice
|
||||||
'name' => 'text128',
|
'name' => 'text128',
|
||||||
'nameIndex' => 'bytes12',
|
'nameIndex' => 'bytes12',
|
||||||
'mailKey' => 'bytes20',
|
'mailKey' => 'bytes20',
|
||||||
|
'isLocked' => 'bool',
|
||||||
),
|
),
|
||||||
self::CONFIG_KEY_SCHEMA => array(
|
self::CONFIG_KEY_SCHEMA => array(
|
||||||
'key_name' => array(
|
'key_name' => array(
|
||||||
|
@ -67,6 +70,37 @@ final class AlmanacDevice
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 )------------------------------------------- */
|
/* -( AlmanacPropertyInterface )------------------------------------------- */
|
||||||
|
|
||||||
|
|
||||||
|
@ -117,7 +151,11 @@ final class AlmanacDevice
|
||||||
case PhabricatorPolicyCapability::CAN_VIEW:
|
case PhabricatorPolicyCapability::CAN_VIEW:
|
||||||
return $this->getViewPolicy();
|
return $this->getViewPolicy();
|
||||||
case PhabricatorPolicyCapability::CAN_EDIT:
|
case PhabricatorPolicyCapability::CAN_EDIT:
|
||||||
return $this->getEditPolicy();
|
if ($this->getIsLocked()) {
|
||||||
|
return PhabricatorPolicies::POLICY_NOONE;
|
||||||
|
} else {
|
||||||
|
return $this->getEditPolicy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,6 +164,14 @@ final class AlmanacDevice
|
||||||
}
|
}
|
||||||
|
|
||||||
public function describeAutomaticCapability($capability) {
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -92,12 +92,21 @@ final class AlmanacInterface
|
||||||
}
|
}
|
||||||
|
|
||||||
public function describeAutomaticCapability($capability) {
|
public function describeAutomaticCapability($capability) {
|
||||||
return array(
|
$notes = array(
|
||||||
pht('An interface inherits the policies of the device it belongs to.'),
|
pht('An interface inherits the policies of the device it belongs to.'),
|
||||||
pht(
|
pht(
|
||||||
'You must be able to view the network an interface resides on to '.
|
'You must be able to view the network an interface resides on to '.
|
||||||
'view the interface.'),
|
'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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ final class AlmanacService
|
||||||
protected $viewPolicy;
|
protected $viewPolicy;
|
||||||
protected $editPolicy;
|
protected $editPolicy;
|
||||||
protected $serviceClass;
|
protected $serviceClass;
|
||||||
|
protected $isLocked;
|
||||||
|
|
||||||
private $customFields = self::ATTACHABLE;
|
private $customFields = self::ATTACHABLE;
|
||||||
private $almanacProperties = self::ATTACHABLE;
|
private $almanacProperties = self::ATTACHABLE;
|
||||||
|
@ -25,7 +26,8 @@ final class AlmanacService
|
||||||
return id(new AlmanacService())
|
return id(new AlmanacService())
|
||||||
->setViewPolicy(PhabricatorPolicies::POLICY_USER)
|
->setViewPolicy(PhabricatorPolicies::POLICY_USER)
|
||||||
->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN)
|
->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN)
|
||||||
->attachAlmanacProperties(array());
|
->attachAlmanacProperties(array())
|
||||||
|
->setIsLocked(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getConfiguration() {
|
public function getConfiguration() {
|
||||||
|
@ -36,6 +38,7 @@ final class AlmanacService
|
||||||
'nameIndex' => 'bytes12',
|
'nameIndex' => 'bytes12',
|
||||||
'mailKey' => 'bytes20',
|
'mailKey' => 'bytes20',
|
||||||
'serviceClass' => 'text64',
|
'serviceClass' => 'text64',
|
||||||
|
'isLocked' => 'bool',
|
||||||
),
|
),
|
||||||
self::CONFIG_KEY_SCHEMA => array(
|
self::CONFIG_KEY_SCHEMA => array(
|
||||||
'key_name' => array(
|
'key_name' => array(
|
||||||
|
@ -141,7 +144,11 @@ final class AlmanacService
|
||||||
case PhabricatorPolicyCapability::CAN_VIEW:
|
case PhabricatorPolicyCapability::CAN_VIEW:
|
||||||
return $this->getViewPolicy();
|
return $this->getViewPolicy();
|
||||||
case PhabricatorPolicyCapability::CAN_EDIT:
|
case PhabricatorPolicyCapability::CAN_EDIT:
|
||||||
return $this->getEditPolicy();
|
if ($this->getIsLocked()) {
|
||||||
|
return PhabricatorPolicies::POLICY_NOONE;
|
||||||
|
} else {
|
||||||
|
return $this->getEditPolicy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,6 +157,14 @@ final class AlmanacService
|
||||||
}
|
}
|
||||||
|
|
||||||
public function describeAutomaticCapability($capability) {
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ final class AlmanacServiceTransaction
|
||||||
extends PhabricatorApplicationTransaction {
|
extends PhabricatorApplicationTransaction {
|
||||||
|
|
||||||
const TYPE_NAME = 'almanac:service:name';
|
const TYPE_NAME = 'almanac:service:name';
|
||||||
|
const TYPE_LOCK = 'almanac:service:lock';
|
||||||
|
|
||||||
public function getApplicationName() {
|
public function getApplicationName() {
|
||||||
return 'almanac';
|
return 'almanac';
|
||||||
|
@ -37,6 +38,17 @@ final class AlmanacServiceTransaction
|
||||||
$new);
|
$new);
|
||||||
}
|
}
|
||||||
break;
|
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();
|
return parent::getTitle();
|
||||||
|
|
|
@ -4,6 +4,7 @@ final class AlmanacInterfaceTableView extends AphrontView {
|
||||||
|
|
||||||
private $interfaces;
|
private $interfaces;
|
||||||
private $handles;
|
private $handles;
|
||||||
|
private $canEdit;
|
||||||
|
|
||||||
public function setHandles(array $handles) {
|
public function setHandles(array $handles) {
|
||||||
$this->handles = $handles;
|
$this->handles = $handles;
|
||||||
|
@ -23,11 +24,26 @@ final class AlmanacInterfaceTableView extends AphrontView {
|
||||||
return $this->interfaces;
|
return $this->interfaces;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setCanEdit($can_edit) {
|
||||||
|
$this->canEdit = $can_edit;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCanEdit() {
|
||||||
|
return $this->canEdit;
|
||||||
|
}
|
||||||
|
|
||||||
public function render() {
|
public function render() {
|
||||||
$interfaces = $this->getInterfaces();
|
$interfaces = $this->getInterfaces();
|
||||||
$handles = $this->getHandles();
|
$handles = $this->getHandles();
|
||||||
$viewer = $this->getUser();
|
$viewer = $this->getUser();
|
||||||
|
|
||||||
|
if ($this->getCanEdit()) {
|
||||||
|
$button_class = 'small grey button';
|
||||||
|
} else {
|
||||||
|
$button_class = 'small grey button disabled';
|
||||||
|
}
|
||||||
|
|
||||||
$rows = array();
|
$rows = array();
|
||||||
foreach ($interfaces as $interface) {
|
foreach ($interfaces as $interface) {
|
||||||
$rows[] = array(
|
$rows[] = array(
|
||||||
|
@ -38,7 +54,7 @@ final class AlmanacInterfaceTableView extends AphrontView {
|
||||||
phutil_tag(
|
phutil_tag(
|
||||||
'a',
|
'a',
|
||||||
array(
|
array(
|
||||||
'class' => 'small grey button',
|
'class' => $button_class,
|
||||||
'href' => '/almanac/interface/edit/'.$interface->getID().'/',
|
'href' => '/almanac/interface/edit/'.$interface->getID().'/',
|
||||||
),
|
),
|
||||||
pht('Edit')),
|
pht('Edit')),
|
||||||
|
|
31
src/docs/user/configuration/cluster.diviner
Normal file
31
src/docs/user/configuration/cluster.diviner
Normal file
|
@ -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}.
|
40
src/docs/user/userguide/almanac.diviner
Normal file
40
src/docs/user/userguide/almanac.diviner
Normal file
|
@ -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.
|
Loading…
Reference in a new issue