1
0
Fork 0
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:
epriestley 2014-12-18 14:31:36 -08:00
parent cd6f67ef95
commit d2df3064bc
24 changed files with 548 additions and 14 deletions

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_almanac.almanac_device
ADD isLocked BOOL NOT NULL;

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_almanac.almanac_service
ADD isLocked BOOL NOT NULL;

View file

@ -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',

View file

@ -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;
}

View file

@ -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);

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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();

View file

@ -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);

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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));
}
}

View file

@ -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);

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -55,6 +55,10 @@ abstract class AlmanacServiceType extends Phobject {
return array();
}
public function getStatusMessages(AlmanacService $service) {
return array();
}
/**
* List all available service type implementations.
*

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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();

View file

@ -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')),

View 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}.

View 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.