mirror of
https://we.phorge.it/source/phorge.git
synced 2025-02-05 03:18:25 +01:00
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
24 changed files with 548 additions and 14 deletions
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',
|
||||
'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 @@ phutil_register_library_map(array(
|
|||
'AlmanacInterfacePHIDType' => 'PhabricatorPHIDType',
|
||||
'AlmanacInterfaceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
|
||||
'AlmanacInterfaceTableView' => 'AphrontView',
|
||||
'AlmanacManagementLockWorkflow' => 'AlmanacManagementWorkflow',
|
||||
'AlmanacManagementTrustKeyWorkflow' => 'AlmanacManagementWorkflow',
|
||||
'AlmanacManagementUnlockWorkflow' => 'AlmanacManagementWorkflow',
|
||||
'AlmanacManagementUntrustKeyWorkflow' => 'AlmanacManagementWorkflow',
|
||||
'AlmanacManagementWorkflow' => 'PhabricatorManagementWorkflow',
|
||||
'AlmanacNames' => 'Phobject',
|
||||
|
|
|
@ -26,6 +26,10 @@ final class PhabricatorAlmanacApplication extends PhabricatorApplication {
|
|||
return self::GROUP_UTILITIES;
|
||||
}
|
||||
|
||||
public function getHelpURI() {
|
||||
return PhabricatorEnv::getDoclink('Almanac User Guide');
|
||||
}
|
||||
|
||||
public function isPrototype() {
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -38,6 +38,14 @@ final class AlmanacBindingViewController
|
|||
->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);
|
||||
|
|
|
@ -179,4 +179,23 @@ abstract class AlmanacController
|
|||
->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();
|
||||
}
|
||||
|
||||
// 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 @@ final class AlmanacDeviceViewController
|
|||
->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 @@ final class AlmanacDeviceViewController
|
|||
$interfaces,
|
||||
$this->buildAlmanacPropertiesTable($device),
|
||||
$this->buildSSHKeysTable($device),
|
||||
$this->buildServicesTable($device),
|
||||
$timeline,
|
||||
),
|
||||
array(
|
||||
|
@ -116,7 +129,8 @@ final class AlmanacDeviceViewController
|
|||
$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 @@ 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)
|
||||
->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();
|
||||
|
|
|
@ -15,6 +15,8 @@ final class AlmanacServiceEditor
|
|||
$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 @@ final class AlmanacServiceEditor
|
|||
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 @@ final class AlmanacServiceEditor
|
|||
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 @@ final class AlmanacServiceEditor
|
|||
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 @@ final class AlmanacServiceEditor
|
|||
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);
|
||||
|
|
|
@ -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
|
||||
|
||||
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 $names;
|
||||
private $serviceClasses;
|
||||
private $devicePHIDs;
|
||||
private $locked;
|
||||
|
||||
private $needBindings;
|
||||
|
||||
public function withIDs(array $ids) {
|
||||
|
@ -29,6 +32,16 @@ final class AlmanacServiceQuery
|
|||
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 @@ final class AlmanacServiceQuery
|
|||
|
||||
$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 @@ final class AlmanacServiceQuery
|
|||
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 @@ final class AlmanacServiceQuery
|
|||
|
||||
$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);
|
||||
|
|
|
@ -78,6 +78,15 @@ final class AlmanacServiceSearchEngine
|
|||
$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);
|
||||
}
|
||||
|
||||
|
|
|
@ -11,4 +11,28 @@ abstract class AlmanacClusterServiceType
|
|||
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();
|
||||
}
|
||||
|
||||
public function getStatusMessages(AlmanacService $service) {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* List all available service type implementations.
|
||||
*
|
||||
|
|
|
@ -143,12 +143,21 @@ final class AlmanacBinding
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ final class AlmanacDevice
|
|||
protected $mailKey;
|
||||
protected $viewPolicy;
|
||||
protected $editPolicy;
|
||||
protected $isLocked;
|
||||
|
||||
private $customFields = self::ATTACHABLE;
|
||||
private $almanacProperties = self::ATTACHABLE;
|
||||
|
@ -23,7 +24,8 @@ final class AlmanacDevice
|
|||
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 @@ final class AlmanacDevice
|
|||
'name' => 'text128',
|
||||
'nameIndex' => 'bytes12',
|
||||
'mailKey' => 'bytes20',
|
||||
'isLocked' => 'bool',
|
||||
),
|
||||
self::CONFIG_KEY_SCHEMA => 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 )------------------------------------------- */
|
||||
|
||||
|
||||
|
@ -117,7 +151,11 @@ final class AlmanacDevice
|
|||
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 @@ final class AlmanacDevice
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -92,12 +92,21 @@ final class AlmanacInterface
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ final class AlmanacService
|
|||
protected $viewPolicy;
|
||||
protected $editPolicy;
|
||||
protected $serviceClass;
|
||||
protected $isLocked;
|
||||
|
||||
private $customFields = self::ATTACHABLE;
|
||||
private $almanacProperties = self::ATTACHABLE;
|
||||
|
@ -25,7 +26,8 @@ final class AlmanacService
|
|||
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 @@ final class AlmanacService
|
|||
'nameIndex' => 'bytes12',
|
||||
'mailKey' => 'bytes20',
|
||||
'serviceClass' => 'text64',
|
||||
'isLocked' => 'bool',
|
||||
),
|
||||
self::CONFIG_KEY_SCHEMA => array(
|
||||
'key_name' => array(
|
||||
|
@ -141,7 +144,11 @@ final class AlmanacService
|
|||
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 @@ final class AlmanacService
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ final class AlmanacServiceTransaction
|
|||
extends PhabricatorApplicationTransaction {
|
||||
|
||||
const TYPE_NAME = 'almanac:service:name';
|
||||
const TYPE_LOCK = 'almanac:service:lock';
|
||||
|
||||
public function getApplicationName() {
|
||||
return 'almanac';
|
||||
|
@ -37,6 +38,17 @@ final class AlmanacServiceTransaction
|
|||
$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();
|
||||
|
|
|
@ -4,6 +4,7 @@ final class AlmanacInterfaceTableView extends AphrontView {
|
|||
|
||||
private $interfaces;
|
||||
private $handles;
|
||||
private $canEdit;
|
||||
|
||||
public function setHandles(array $handles) {
|
||||
$this->handles = $handles;
|
||||
|
@ -23,11 +24,26 @@ final class AlmanacInterfaceTableView extends AphrontView {
|
|||
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 @@ final class AlmanacInterfaceTableView extends AphrontView {
|
|||
phutil_tag(
|
||||
'a',
|
||||
array(
|
||||
'class' => 'small grey button',
|
||||
'class' => $button_class,
|
||||
'href' => '/almanac/interface/edit/'.$interface->getID().'/',
|
||||
),
|
||||
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…
Add table
Reference in a new issue