@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+548 -14
+2
resources/sql/autopatches/20141217.almanacdevicelock.sql
··· 1 + ALTER TABLE {$NAMESPACE}_almanac.almanac_device 2 + ADD isLocked BOOL NOT NULL;
+2
resources/sql/autopatches/20141217.almanaclock.sql
··· 1 + ALTER TABLE {$NAMESPACE}_almanac.almanac_service 2 + ADD isLocked BOOL NOT NULL;
+4
src/__phutil_library_map__.php
··· 49 49 'AlmanacInterfacePHIDType' => 'applications/almanac/phid/AlmanacInterfacePHIDType.php', 50 50 'AlmanacInterfaceQuery' => 'applications/almanac/query/AlmanacInterfaceQuery.php', 51 51 'AlmanacInterfaceTableView' => 'applications/almanac/view/AlmanacInterfaceTableView.php', 52 + 'AlmanacManagementLockWorkflow' => 'applications/almanac/management/AlmanacManagementLockWorkflow.php', 52 53 'AlmanacManagementTrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementTrustKeyWorkflow.php', 54 + 'AlmanacManagementUnlockWorkflow' => 'applications/almanac/management/AlmanacManagementUnlockWorkflow.php', 53 55 'AlmanacManagementUntrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementUntrustKeyWorkflow.php', 54 56 'AlmanacManagementWorkflow' => 'applications/almanac/management/AlmanacManagementWorkflow.php', 55 57 'AlmanacNames' => 'applications/almanac/util/AlmanacNames.php', ··· 3068 3070 'AlmanacInterfacePHIDType' => 'PhabricatorPHIDType', 3069 3071 'AlmanacInterfaceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 3070 3072 'AlmanacInterfaceTableView' => 'AphrontView', 3073 + 'AlmanacManagementLockWorkflow' => 'AlmanacManagementWorkflow', 3071 3074 'AlmanacManagementTrustKeyWorkflow' => 'AlmanacManagementWorkflow', 3075 + 'AlmanacManagementUnlockWorkflow' => 'AlmanacManagementWorkflow', 3072 3076 'AlmanacManagementUntrustKeyWorkflow' => 'AlmanacManagementWorkflow', 3073 3077 'AlmanacManagementWorkflow' => 'PhabricatorManagementWorkflow', 3074 3078 'AlmanacNames' => 'Phobject',
+4
src/applications/almanac/application/PhabricatorAlmanacApplication.php
··· 26 26 return self::GROUP_UTILITIES; 27 27 } 28 28 29 + public function getHelpURI() { 30 + return PhabricatorEnv::getDoclink('Almanac User Guide'); 31 + } 32 + 29 33 public function isPrototype() { 30 34 return true; 31 35 }
+8
src/applications/almanac/controller/AlmanacBindingViewController.php
··· 38 38 ->setHeader($header) 39 39 ->addPropertyList($property_list); 40 40 41 + if ($binding->getService()->getIsLocked()) { 42 + $this->addLockMessage( 43 + $box, 44 + pht( 45 + 'This service for this binding is locked, so the binding can '. 46 + 'not be edited.')); 47 + } 48 + 41 49 $crumbs = $this->buildApplicationCrumbs(); 42 50 $crumbs->addTextCrumb($service->getName(), $service_uri); 43 51 $crumbs->addTextCrumb($title);
+19
src/applications/almanac/controller/AlmanacController.php
··· 179 179 ->appendChild($table); 180 180 } 181 181 182 + protected function addLockMessage(PHUIObjectBoxView $box, $message) { 183 + $doc_link = phutil_tag( 184 + 'a', 185 + array( 186 + 'href' => PhabricatorEnv::getDoclink('Almanac User Guide'), 187 + 'target' => '_blank', 188 + ), 189 + pht('Learn More')); 190 + 191 + $error_view = id(new AphrontErrorView()) 192 + ->setSeverity(AphrontErrorView::SEVERITY_WARNING) 193 + ->setErrors( 194 + array( 195 + array($message, ' ', $doc_link), 196 + )); 197 + 198 + $box->setErrorView($error_view); 199 + } 200 + 182 201 }
+63 -1
src/applications/almanac/controller/AlmanacDeviceViewController.php
··· 20 20 return new Aphront404Response(); 21 21 } 22 22 23 + // We rebuild locks on a device when viewing the detail page, so they 24 + // automatically get corrected if they fall out of sync. 25 + $device->rebuildDeviceLocks(); 26 + 23 27 $title = pht('Device %s', $device->getName()); 24 28 25 29 $property_list = $this->buildPropertyList($device); ··· 35 39 ->setHeader($header) 36 40 ->addPropertyList($property_list); 37 41 42 + if ($device->getIsLocked()) { 43 + $this->addLockMessage( 44 + $box, 45 + pht( 46 + 'This device is bound to a locked service, so it can not be '. 47 + 'edited.')); 48 + } 49 + 38 50 $interfaces = $this->buildInterfaceList($device); 39 51 40 52 $crumbs = $this->buildApplicationCrumbs(); ··· 52 64 $interfaces, 53 65 $this->buildAlmanacPropertiesTable($device), 54 66 $this->buildSSHKeysTable($device), 67 + $this->buildServicesTable($device), 55 68 $timeline, 56 69 ), 57 70 array( ··· 116 129 $table = id(new AlmanacInterfaceTableView()) 117 130 ->setUser($viewer) 118 131 ->setInterfaces($interfaces) 119 - ->setHandles($handles); 132 + ->setHandles($handles) 133 + ->setCanEdit($can_edit); 120 134 121 135 $header = id(new PHUIHeaderView()) 122 136 ->setHeader(pht('Device Interfaces')) ··· 198 212 199 213 200 214 } 215 + 216 + private function buildServicesTable(AlmanacDevice $device) { 217 + 218 + // NOTE: We're loading all services so we can show hidden, locked services. 219 + // In general, we let you know about all the things the device is bound to, 220 + // even if you don't have permission to see their details. This is similar 221 + // to exposing the existence of edges in other applications, with the 222 + // addition of always letting you see that locks exist. 223 + 224 + $services = id(new AlmanacServiceQuery()) 225 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 226 + ->withDevicePHIDs(array($device->getPHID())) 227 + ->execute(); 228 + 229 + $handles = $this->loadViewerHandles(mpull($services, 'getPHID')); 230 + 231 + $icon_lock = id(new PHUIIconView()) 232 + ->setIconFont('fa-lock'); 233 + 234 + $rows = array(); 235 + foreach ($services as $service) { 236 + $handle = $handles[$service->getPHID()]; 237 + $rows[] = array( 238 + ($service->getIsLocked() 239 + ? $icon_lock 240 + : null), 241 + $handle->renderLink(), 242 + ); 243 + } 244 + 245 + $table = id(new AphrontTableView($rows)) 246 + ->setNoDataString(pht('No services are bound to this device.')) 247 + ->setHeaders( 248 + array( 249 + null, 250 + pht('Service'), 251 + )) 252 + ->setColumnClasses( 253 + array( 254 + null, 255 + 'wide pri', 256 + )); 257 + 258 + return id(new PHUIObjectBoxView()) 259 + ->setHeaderText(pht('Bound Services')) 260 + ->appendChild($table); 261 + } 262 + 201 263 202 264 }
+11
src/applications/almanac/controller/AlmanacServiceViewController.php
··· 35 35 ->setHeader($header) 36 36 ->addPropertyList($property_list); 37 37 38 + $messages = $service->getServiceType()->getStatusMessages($service); 39 + if ($messages) { 40 + $box->setFormErrors($messages); 41 + } 42 + 43 + if ($service->getIsLocked()) { 44 + $this->addLockMessage( 45 + $box, 46 + pht('This service is locked, and can not be edited.')); 47 + } 48 + 38 49 $bindings = $this->buildBindingList($service); 39 50 40 51 $crumbs = $this->buildApplicationCrumbs();
+26
src/applications/almanac/editor/AlmanacServiceEditor.php
··· 15 15 $types = parent::getTransactionTypes(); 16 16 17 17 $types[] = AlmanacServiceTransaction::TYPE_NAME; 18 + $types[] = AlmanacServiceTransaction::TYPE_LOCK; 19 + 18 20 $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; 19 21 $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; 20 22 ··· 27 29 switch ($xaction->getTransactionType()) { 28 30 case AlmanacServiceTransaction::TYPE_NAME: 29 31 return $object->getName(); 32 + case AlmanacServiceTransaction::TYPE_LOCK: 33 + return (bool)$object->getIsLocked(); 30 34 } 31 35 32 36 return parent::getCustomTransactionOldValue($object, $xaction); ··· 39 43 switch ($xaction->getTransactionType()) { 40 44 case AlmanacServiceTransaction::TYPE_NAME: 41 45 return $xaction->getNewValue(); 46 + case AlmanacServiceTransaction::TYPE_LOCK: 47 + return (bool)$xaction->getNewValue(); 42 48 } 43 49 44 50 return parent::getCustomTransactionNewValue($object, $xaction); ··· 52 58 case AlmanacServiceTransaction::TYPE_NAME: 53 59 $object->setName($xaction->getNewValue()); 54 60 return; 61 + case AlmanacServiceTransaction::TYPE_LOCK: 62 + $object->setIsLocked((int)$xaction->getNewValue()); 63 + return; 55 64 case PhabricatorTransactions::TYPE_VIEW_POLICY: 56 65 case PhabricatorTransactions::TYPE_EDIT_POLICY: 57 66 case PhabricatorTransactions::TYPE_EDGE: ··· 70 79 case PhabricatorTransactions::TYPE_VIEW_POLICY: 71 80 case PhabricatorTransactions::TYPE_EDIT_POLICY: 72 81 case PhabricatorTransactions::TYPE_EDGE: 82 + return; 83 + case AlmanacServiceTransaction::TYPE_LOCK: 84 + $service = id(new AlmanacServiceQuery()) 85 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 86 + ->withPHIDs(array($object->getPHID())) 87 + ->needBindings(true) 88 + ->executeOne(); 89 + 90 + $devices = array(); 91 + foreach ($service->getBindings() as $binding) { 92 + $device = $binding->getInterface()->getDevice(); 93 + $devices[$device->getPHID()] = $device; 94 + } 95 + 96 + foreach ($devices as $device) { 97 + $device->rebuildDeviceLocks(); 98 + } 73 99 return; 74 100 } 75 101
+49
src/applications/almanac/management/AlmanacManagementLockWorkflow.php
··· 1 + <?php 2 + 3 + final class AlmanacManagementLockWorkflow 4 + extends AlmanacManagementWorkflow { 5 + 6 + public function didConstruct() { 7 + $this 8 + ->setName('lock') 9 + ->setSynopsis(pht('Lock a service to prevent it from being edited.')) 10 + ->setArguments( 11 + array( 12 + array( 13 + 'name' => 'services', 14 + 'wildcard' => true, 15 + ), 16 + )); 17 + } 18 + 19 + public function execute(PhutilArgumentParser $args) { 20 + $console = PhutilConsole::getConsole(); 21 + 22 + $services = $this->loadServices($args->getArg('services')); 23 + if (!$services) { 24 + throw new PhutilArgumentUsageException( 25 + pht('Specify at least one service to lock.')); 26 + } 27 + 28 + foreach ($services as $service) { 29 + if ($service->getIsLocked()) { 30 + throw new PhutilArgumentUsageException( 31 + pht( 32 + 'Service "%s" is already locked!', 33 + $service->getName())); 34 + } 35 + } 36 + 37 + foreach ($services as $service) { 38 + $this->updateServiceLock($service, true); 39 + 40 + $console->writeOut( 41 + "**<bg:green> %s </bg>** %s\n", 42 + pht('LOCKED'), 43 + pht('Service "%s" was locked.', $service->getName())); 44 + } 45 + 46 + return 0; 47 + } 48 + 49 + }
+49
src/applications/almanac/management/AlmanacManagementUnlockWorkflow.php
··· 1 + <?php 2 + 3 + final class AlmanacManagementUnlockWorkflow 4 + extends AlmanacManagementWorkflow { 5 + 6 + public function didConstruct() { 7 + $this 8 + ->setName('unlock') 9 + ->setSynopsis(pht('Unlock a service to allow it to be edited.')) 10 + ->setArguments( 11 + array( 12 + array( 13 + 'name' => 'services', 14 + 'wildcard' => true, 15 + ), 16 + )); 17 + } 18 + 19 + public function execute(PhutilArgumentParser $args) { 20 + $console = PhutilConsole::getConsole(); 21 + 22 + $services = $this->loadServices($args->getArg('services')); 23 + if (!$services) { 24 + throw new PhutilArgumentUsageException( 25 + pht('Specify at least one service to unlock.')); 26 + } 27 + 28 + foreach ($services as $service) { 29 + if (!$service->getIsLocked()) { 30 + throw new PhutilArgumentUsageException( 31 + pht( 32 + 'Service "%s" is not locked!', 33 + $service->getName())); 34 + } 35 + } 36 + 37 + foreach ($services as $service) { 38 + $this->updateServiceLock($service, false); 39 + 40 + $console->writeOut( 41 + "**<bg:green> %s </bg>** %s\n", 42 + pht('UNLOCKED'), 43 + pht('Service "%s" was unlocked.', $service->getName())); 44 + } 45 + 46 + return 0; 47 + } 48 + 49 + }
+43 -1
src/applications/almanac/management/AlmanacManagementWorkflow.php
··· 1 1 <?php 2 2 3 3 abstract class AlmanacManagementWorkflow 4 - extends PhabricatorManagementWorkflow {} 4 + extends PhabricatorManagementWorkflow { 5 + 6 + 7 + protected function loadServices(array $names) { 8 + if (!$names) { 9 + return array(); 10 + } 11 + 12 + $services = id(new AlmanacServiceQuery()) 13 + ->setViewer($this->getViewer()) 14 + ->withNames($names) 15 + ->execute(); 16 + 17 + $services = mpull($services, null, 'getName'); 18 + foreach ($names as $name) { 19 + if (empty($services[$name])) { 20 + throw new PhutilArgumentUsageException( 21 + pht( 22 + 'Service "%s" does not exist or could not be loaded!', 23 + $name)); 24 + } 25 + } 26 + 27 + return $services; 28 + } 29 + 30 + protected function updateServiceLock(AlmanacService $service, $lock) { 31 + $almanac_phid = id(new PhabricatorAlmanacApplication())->getPHID(); 32 + 33 + $xaction = id(new AlmanacServiceTransaction()) 34 + ->setTransactionType(AlmanacServiceTransaction::TYPE_LOCK) 35 + ->setNewValue((int)$lock); 36 + 37 + $editor = id(new AlmanacServiceEditor()) 38 + ->setActor($this->getViewer()) 39 + ->setActingAsPHID($almanac_phid) 40 + ->setContentSource(PhabricatorContentSource::newConsoleSource()) 41 + ->setContinueOnMissingFields(true); 42 + 43 + $editor->applyTransactions($service, array($xaction)); 44 + } 45 + 46 + }
+46 -5
src/applications/almanac/query/AlmanacServiceQuery.php
··· 7 7 private $phids; 8 8 private $names; 9 9 private $serviceClasses; 10 + private $devicePHIDs; 11 + private $locked; 12 + 10 13 private $needBindings; 11 14 12 15 public function withIDs(array $ids) { ··· 26 29 27 30 public function withServiceClasses(array $classes) { 28 31 $this->serviceClasses = $classes; 32 + return $this; 33 + } 34 + 35 + public function withDevicePHIDs(array $phids) { 36 + $this->devicePHIDs = $phids; 37 + return $this; 38 + } 39 + 40 + public function withLocked($locked) { 41 + $this->locked = $locked; 29 42 return $this; 30 43 } 31 44 ··· 40 53 41 54 $data = queryfx_all( 42 55 $conn_r, 43 - 'SELECT * FROM %T %Q %Q %Q', 56 + 'SELECT service.* FROM %T service %Q %Q %Q %Q', 44 57 $table->getTableName(), 58 + $this->buildJoinClause($conn_r), 45 59 $this->buildWhereClause($conn_r), 46 60 $this->buildOrderClause($conn_r), 47 61 $this->buildLimitClause($conn_r)); ··· 49 63 return $table->loadAllFromArray($data); 50 64 } 51 65 66 + protected function buildJoinClause($conn_r) { 67 + $joins = array(); 68 + 69 + if ($this->devicePHIDs !== null) { 70 + $joins[] = qsprintf( 71 + $conn_r, 72 + 'JOIN %T binding ON service.phid = binding.servicePHID', 73 + id(new AlmanacBinding())->getTableName()); 74 + } 75 + 76 + return implode(' ', $joins); 77 + } 78 + 52 79 protected function buildWhereClause($conn_r) { 53 80 $where = array(); 54 81 55 82 if ($this->ids !== null) { 56 83 $where[] = qsprintf( 57 84 $conn_r, 58 - 'id IN (%Ld)', 85 + 'service.id IN (%Ld)', 59 86 $this->ids); 60 87 } 61 88 62 89 if ($this->phids !== null) { 63 90 $where[] = qsprintf( 64 91 $conn_r, 65 - 'phid IN (%Ls)', 92 + 'service.phid IN (%Ls)', 66 93 $this->phids); 67 94 } 68 95 ··· 74 101 75 102 $where[] = qsprintf( 76 103 $conn_r, 77 - 'nameIndex IN (%Ls)', 104 + 'service.nameIndex IN (%Ls)', 78 105 $hashes); 79 106 } 80 107 81 108 if ($this->serviceClasses !== null) { 82 109 $where[] = qsprintf( 83 110 $conn_r, 84 - 'serviceClass IN (%Ls)', 111 + 'service.serviceClass IN (%Ls)', 85 112 $this->serviceClasses); 113 + } 114 + 115 + if ($this->devicePHIDs !== null) { 116 + $where[] = qsprintf( 117 + $conn_r, 118 + 'binding.devicePHID IN (%Ls)', 119 + $this->devicePHIDs); 120 + } 121 + 122 + if ($this->locked !== null) { 123 + $where[] = qsprintf( 124 + $conn_r, 125 + 'service.isLocked = %d', 126 + (int)$this->locked); 86 127 } 87 128 88 129 $where[] = $this->buildPagingClause($conn_r);
+9
src/applications/almanac/query/AlmanacServiceSearchEngine.php
··· 78 78 $service->getServiceType()->getServiceTypeIcon(), 79 79 $service->getServiceType()->getServiceTypeShortName()); 80 80 81 + if ($service->getIsLocked() || 82 + $service->getServiceType()->isClusterServiceType()) { 83 + if ($service->getIsLocked()) { 84 + $item->addIcon('fa-lock', pht('Locked')); 85 + } else { 86 + $item->addIcon('fa-unlock-alt red', pht('Unlocked')); 87 + } 88 + } 89 + 81 90 $list->addItem($item); 82 91 } 83 92
+24
src/applications/almanac/servicetype/AlmanacClusterServiceType.php
··· 11 11 return 'fa-sitemap'; 12 12 } 13 13 14 + public function getStatusMessages(AlmanacService $service) { 15 + $messages = parent::getStatusMessages($service); 16 + 17 + if (!$service->getIsLocked()) { 18 + $doc_href = PhabricatorEnv::getDoclink( 19 + 'User Guide: Phabricator Clusters'); 20 + 21 + $doc_link = phutil_tag( 22 + 'a', 23 + array( 24 + 'href' => $doc_href, 25 + 'target' => '_blank', 26 + ), 27 + pht('Learn More')); 28 + 29 + $messages[] = pht( 30 + 'This is an unlocked cluster service. After you finish editing '. 31 + 'it, you should lock it. %s.', 32 + $doc_link); 33 + } 34 + 35 + return $messages; 36 + } 37 + 14 38 }
+4
src/applications/almanac/servicetype/AlmanacServiceType.php
··· 55 55 return array(); 56 56 } 57 57 58 + public function getStatusMessages(AlmanacService $service) { 59 + return array(); 60 + } 61 + 58 62 /** 59 63 * List all available service type implementations. 60 64 *
+10 -1
src/applications/almanac/storage/AlmanacBinding.php
··· 143 143 } 144 144 145 145 public function describeAutomaticCapability($capability) { 146 - return array( 146 + $notes = array( 147 147 pht('A binding inherits the policies of its service.'), 148 148 pht( 149 149 'To view a binding, you must also be able to view its device and '. 150 150 'interface.'), 151 151 ); 152 + 153 + if ($capability === PhabricatorPolicyCapability::CAN_EDIT) { 154 + if ($this->getService()->getIsLocked()) { 155 + $notes[] = pht( 156 + 'The service for this binding is locked, so it can not be edited.'); 157 + } 158 + } 159 + 160 + return $notes; 152 161 } 153 162 154 163
+48 -2
src/applications/almanac/storage/AlmanacDevice.php
··· 15 15 protected $mailKey; 16 16 protected $viewPolicy; 17 17 protected $editPolicy; 18 + protected $isLocked; 18 19 19 20 private $customFields = self::ATTACHABLE; 20 21 private $almanacProperties = self::ATTACHABLE; ··· 23 24 return id(new AlmanacDevice()) 24 25 ->setViewPolicy(PhabricatorPolicies::POLICY_USER) 25 26 ->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN) 26 - ->attachAlmanacProperties(array()); 27 + ->attachAlmanacProperties(array()) 28 + ->setIsLocked(0); 27 29 } 28 30 29 31 public function getConfiguration() { ··· 33 35 'name' => 'text128', 34 36 'nameIndex' => 'bytes12', 35 37 'mailKey' => 'bytes20', 38 + 'isLocked' => 'bool', 36 39 ), 37 40 self::CONFIG_KEY_SCHEMA => array( 38 41 'key_name' => array( ··· 67 70 } 68 71 69 72 73 + /** 74 + * Find locked services which are bound to this device, updating the device 75 + * lock flag if necessary. 76 + * 77 + * @return list<phid> List of locking service PHIDs. 78 + */ 79 + public function rebuildDeviceLocks() { 80 + $services = id(new AlmanacServiceQuery()) 81 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 82 + ->withDevicePHIDs(array($this->getPHID())) 83 + ->withLocked(true) 84 + ->execute(); 85 + 86 + $locked = (bool)count($services); 87 + 88 + if ($locked != $this->getIsLocked()) { 89 + $this->setIsLocked((int)$locked); 90 + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 91 + queryfx( 92 + $this->establishConnection('w'), 93 + 'UPDATE %T SET isLocked = %d WHERE id = %d', 94 + $this->getTableName(), 95 + $this->getIsLocked(), 96 + $this->getID()); 97 + unset($unguarded); 98 + } 99 + 100 + return $this; 101 + } 102 + 103 + 70 104 /* -( AlmanacPropertyInterface )------------------------------------------- */ 71 105 72 106 ··· 117 151 case PhabricatorPolicyCapability::CAN_VIEW: 118 152 return $this->getViewPolicy(); 119 153 case PhabricatorPolicyCapability::CAN_EDIT: 120 - return $this->getEditPolicy(); 154 + if ($this->getIsLocked()) { 155 + return PhabricatorPolicies::POLICY_NOONE; 156 + } else { 157 + return $this->getEditPolicy(); 158 + } 121 159 } 122 160 } 123 161 ··· 126 164 } 127 165 128 166 public function describeAutomaticCapability($capability) { 167 + if ($capability === PhabricatorPolicyCapability::CAN_EDIT) { 168 + if ($this->getIsLocked()) { 169 + return pht( 170 + 'This device is bound to a locked service, so it can not '. 171 + 'be edited.'); 172 + } 173 + } 174 + 129 175 return null; 130 176 } 131 177
+10 -1
src/applications/almanac/storage/AlmanacInterface.php
··· 92 92 } 93 93 94 94 public function describeAutomaticCapability($capability) { 95 - return array( 95 + $notes = array( 96 96 pht('An interface inherits the policies of the device it belongs to.'), 97 97 pht( 98 98 'You must be able to view the network an interface resides on to '. 99 99 'view the interface.'), 100 100 ); 101 + 102 + if ($capability === PhabricatorPolicyCapability::CAN_EDIT) { 103 + if ($this->getDevice()->getIsLocked()) { 104 + $notes[] = pht( 105 + 'The device for this interface is locked, so it can not be edited.'); 106 + } 107 + } 108 + 109 + return $notes; 101 110 } 102 111 103 112 }
+17 -2
src/applications/almanac/storage/AlmanacService.php
··· 15 15 protected $viewPolicy; 16 16 protected $editPolicy; 17 17 protected $serviceClass; 18 + protected $isLocked; 18 19 19 20 private $customFields = self::ATTACHABLE; 20 21 private $almanacProperties = self::ATTACHABLE; ··· 25 26 return id(new AlmanacService()) 26 27 ->setViewPolicy(PhabricatorPolicies::POLICY_USER) 27 28 ->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN) 28 - ->attachAlmanacProperties(array()); 29 + ->attachAlmanacProperties(array()) 30 + ->setIsLocked(0); 29 31 } 30 32 31 33 public function getConfiguration() { ··· 36 38 'nameIndex' => 'bytes12', 37 39 'mailKey' => 'bytes20', 38 40 'serviceClass' => 'text64', 41 + 'isLocked' => 'bool', 39 42 ), 40 43 self::CONFIG_KEY_SCHEMA => array( 41 44 'key_name' => array( ··· 141 144 case PhabricatorPolicyCapability::CAN_VIEW: 142 145 return $this->getViewPolicy(); 143 146 case PhabricatorPolicyCapability::CAN_EDIT: 144 - return $this->getEditPolicy(); 147 + if ($this->getIsLocked()) { 148 + return PhabricatorPolicies::POLICY_NOONE; 149 + } else { 150 + return $this->getEditPolicy(); 151 + } 145 152 } 146 153 } 147 154 ··· 150 157 } 151 158 152 159 public function describeAutomaticCapability($capability) { 160 + switch ($capability) { 161 + case PhabricatorPolicyCapability::CAN_EDIT: 162 + if ($this->getIsLocked()) { 163 + return pht('This service is locked and can not be edited.'); 164 + } 165 + break; 166 + } 167 + 153 168 return null; 154 169 } 155 170
+12
src/applications/almanac/storage/AlmanacServiceTransaction.php
··· 4 4 extends PhabricatorApplicationTransaction { 5 5 6 6 const TYPE_NAME = 'almanac:service:name'; 7 + const TYPE_LOCK = 'almanac:service:lock'; 7 8 8 9 public function getApplicationName() { 9 10 return 'almanac'; ··· 35 36 $this->renderHandleLink($author_phid), 36 37 $old, 37 38 $new); 39 + } 40 + break; 41 + case self::TYPE_LOCK: 42 + if ($new) { 43 + return pht( 44 + '%s locked this service.', 45 + $this->renderHandleLink($author_phid)); 46 + } else { 47 + return pht( 48 + '%s unlocked this service.', 49 + $this->renderHandleLink($author_phid)); 38 50 } 39 51 break; 40 52 }
+17 -1
src/applications/almanac/view/AlmanacInterfaceTableView.php
··· 4 4 5 5 private $interfaces; 6 6 private $handles; 7 + private $canEdit; 7 8 8 9 public function setHandles(array $handles) { 9 10 $this->handles = $handles; ··· 23 24 return $this->interfaces; 24 25 } 25 26 27 + public function setCanEdit($can_edit) { 28 + $this->canEdit = $can_edit; 29 + return $this; 30 + } 31 + 32 + public function getCanEdit() { 33 + return $this->canEdit; 34 + } 35 + 26 36 public function render() { 27 37 $interfaces = $this->getInterfaces(); 28 38 $handles = $this->getHandles(); 29 39 $viewer = $this->getUser(); 30 40 41 + if ($this->getCanEdit()) { 42 + $button_class = 'small grey button'; 43 + } else { 44 + $button_class = 'small grey button disabled'; 45 + } 46 + 31 47 $rows = array(); 32 48 foreach ($interfaces as $interface) { 33 49 $rows[] = array( ··· 38 54 phutil_tag( 39 55 'a', 40 56 array( 41 - 'class' => 'small grey button', 57 + 'class' => $button_class, 42 58 'href' => '/almanac/interface/edit/'.$interface->getID().'/', 43 59 ), 44 60 pht('Edit')),
+31
src/docs/user/configuration/cluster.diviner
··· 1 + @title User Guide: Phabricator Clusters 2 + @group config 3 + 4 + Guide on scaling Phabricator across multiple machines, for large installs. 5 + 6 + Overview 7 + ======== 8 + 9 + IMPORTANT: Phabricator clustering is in its infancy and does not work at all 10 + yet. This document is mostly a placeholder. 11 + 12 + Locking Services 13 + ================ 14 + 15 + Because cluster configuration is defined in Phabricator itself, an attacker 16 + who compromises an account that can edit the cluster definition has significant 17 + power. For example, the attacker might be able to configure Phabricator to 18 + replicate the database to a server they control. 19 + 20 + To mitigate this attack, services in Almanac can be locked to prevent them 21 + from being edited from the web UI. An attacker would then need significantly 22 + greater access (to the CLI, or directly to the database) in order to change 23 + the cluster configuration. 24 + 25 + You should normally keep cluster services in a locked state, and unlock them 26 + only to edit them. Once you're finished making changes, lock the service again. 27 + The web UI will warn you when you're viewing an unlocked cluster service, as 28 + a reminder that you should lock it again once you're finished editing. 29 + 30 + For details on how to lock and unlock a service, see 31 + @{article:Almanac User Guide}.
+40
src/docs/user/userguide/almanac.diviner
··· 1 + @title Almanac User Guide 2 + @group userguide 3 + 4 + Using Almanac to manage services. 5 + 6 + = Overview = 7 + 8 + IMPORTANT: Almanac is a prototype application. See 9 + @{article:User Guide: Prototype Applications}. 10 + 11 + Locking and Unlocking Services 12 + ============================== 13 + 14 + Services can be locked to prevent edits from the web UI. This primarily hardens 15 + Almanac against attacks involving account compromise. Notably, locking cluster 16 + services prevents an attacker from modifying the Phabricator cluster definition. 17 + For more details on this scenario, see 18 + @{article:User Guide: Phabricator Clusters}. 19 + 20 + Beyond hardening cluster definitions, you might also want to lock a service to 21 + prevent accidental edits. 22 + 23 + To lock a service, run: 24 + 25 + phabricator/ $ ./bin/almanac lock <service> 26 + 27 + To unlock a service later, run: 28 + 29 + phabricator/ $ ./bin/almanac unlock <service> 30 + 31 + Locking a service also locks all of the service's bindings and properties, as 32 + well as the devices connected to the service. Generally, no part of the 33 + service definition can be modified while it is locked. 34 + 35 + Devices (and their properties) will remain locked as long as they are bound to 36 + at least one locked service. To edit a device, you'll need to unlock all the 37 + services it is bound to. 38 + 39 + Locked services and devices will show that they are locked in the web UI, and 40 + editing options will be unavailable.