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

Simplify locking of Almanac cluster services

Summary:
Fixes T6741. Ref T10246. Broadly, we want to protect Almanac cluster services:

- Today, against users in the Phacility cluster accidentally breaking their own instances.
- In the future, against attackers compromising administrative accounts and adding a new "cluster database" which points at hardware they control.

The way this works right now is really complicated: there's a global "can create cluster services" setting, and then separate per-service and per-device locks.

Instead, change "Can Create Cluster Services" into "Can Manage Cluster Services". Require this permission (in addition to normal permissions) to edit or create any cluster service.

This permission can be locked to "No One" via config (as we do in the Phacility cluster) so we only need this one simple setting.

There's also zero reason to individually lock //some// of the cluster services.

Also improve extended policy errors.

The UI here is still a little heavy-handed, but should be good enough for the moment.

Test Plan:
- Ran migrations.
- Verified that cluster services and bindings reported that they belonged to the cluster.
- Edited a cluster binding.
- Verified that the bound device was marked as a cluster device
- Moved a cluster binding, verified the old device was unmarked as a cluster device.
- Tried to edit a cluster device as an unprivileged user, got a sensible error.

{F1126552}

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T6741, T10246

Differential Revision: https://secure.phabricator.com/D15339

+207 -335
+2
resources/sql/autopatches/20160223.almanac.1.bound.sql
··· 1 + ALTER TABLE {$NAMESPACE}_almanac.almanac_device 2 + ADD isBoundToClusterService BOOL NOT NULL;
+2
resources/sql/autopatches/20160223.almanac.2.lockbind.sql
··· 1 + UPDATE {$NAMESPACE}_almanac.almanac_device 2 + SET isBoundToClusterService = isLocked;
+2
resources/sql/autopatches/20160223.almanac.3.devicelock.sql
··· 1 + ALTER TABLE {$NAMESPACE}_almanac.almanac_device 2 + DROP isLocked;
+2
resources/sql/autopatches/20160223.almanac.4.servicelock.sql
··· 1 + ALTER TABLE {$NAMESPACE}_almanac.almanac_service 2 + DROP isLocked;
+5 -6
src/__phutil_library_map__.php
··· 27 27 'AlmanacConduitAPIMethod' => 'applications/almanac/conduit/AlmanacConduitAPIMethod.php', 28 28 'AlmanacConsoleController' => 'applications/almanac/controller/AlmanacConsoleController.php', 29 29 'AlmanacController' => 'applications/almanac/controller/AlmanacController.php', 30 - 'AlmanacCreateClusterServicesCapability' => 'applications/almanac/capability/AlmanacCreateClusterServicesCapability.php', 31 30 'AlmanacCreateDevicesCapability' => 'applications/almanac/capability/AlmanacCreateDevicesCapability.php', 32 31 'AlmanacCreateNamespacesCapability' => 'applications/almanac/capability/AlmanacCreateNamespacesCapability.php', 33 32 'AlmanacCreateNetworksCapability' => 'applications/almanac/capability/AlmanacCreateNetworksCapability.php', ··· 57 56 'AlmanacInterfaceQuery' => 'applications/almanac/query/AlmanacInterfaceQuery.php', 58 57 'AlmanacInterfaceTableView' => 'applications/almanac/view/AlmanacInterfaceTableView.php', 59 58 'AlmanacKeys' => 'applications/almanac/util/AlmanacKeys.php', 60 - 'AlmanacManagementLockWorkflow' => 'applications/almanac/management/AlmanacManagementLockWorkflow.php', 59 + 'AlmanacManageClusterServicesCapability' => 'applications/almanac/capability/AlmanacManageClusterServicesCapability.php', 61 60 'AlmanacManagementRegisterWorkflow' => 'applications/almanac/management/AlmanacManagementRegisterWorkflow.php', 62 61 'AlmanacManagementTrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementTrustKeyWorkflow.php', 63 - 'AlmanacManagementUnlockWorkflow' => 'applications/almanac/management/AlmanacManagementUnlockWorkflow.php', 64 62 'AlmanacManagementUntrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementUntrustKeyWorkflow.php', 65 63 'AlmanacManagementWorkflow' => 'applications/almanac/management/AlmanacManagementWorkflow.php', 66 64 'AlmanacNames' => 'applications/almanac/util/AlmanacNames.php', ··· 3996 3994 'PhabricatorApplicationTransactionInterface', 3997 3995 'AlmanacPropertyInterface', 3998 3996 'PhabricatorDestructibleInterface', 3997 + 'PhabricatorExtendedPolicyInterface', 3999 3998 ), 4000 3999 'AlmanacBindingEditController' => 'AlmanacServiceController', 4001 4000 'AlmanacBindingEditor' => 'AlmanacEditor', ··· 4013 4012 'AlmanacConduitAPIMethod' => 'ConduitAPIMethod', 4014 4013 'AlmanacConsoleController' => 'AlmanacController', 4015 4014 'AlmanacController' => 'PhabricatorController', 4016 - 'AlmanacCreateClusterServicesCapability' => 'PhabricatorPolicyCapability', 4017 4015 'AlmanacCreateDevicesCapability' => 'PhabricatorPolicyCapability', 4018 4016 'AlmanacCreateNamespacesCapability' => 'PhabricatorPolicyCapability', 4019 4017 'AlmanacCreateNetworksCapability' => 'PhabricatorPolicyCapability', ··· 4030 4028 'PhabricatorDestructibleInterface', 4031 4029 'PhabricatorNgramsInterface', 4032 4030 'PhabricatorConduitResultInterface', 4031 + 'PhabricatorExtendedPolicyInterface', 4033 4032 ), 4034 4033 'AlmanacDeviceController' => 'AlmanacController', 4035 4034 'AlmanacDeviceEditController' => 'AlmanacDeviceController', ··· 4057 4056 'AlmanacInterfaceQuery' => 'AlmanacQuery', 4058 4057 'AlmanacInterfaceTableView' => 'AphrontView', 4059 4058 'AlmanacKeys' => 'Phobject', 4060 - 'AlmanacManagementLockWorkflow' => 'AlmanacManagementWorkflow', 4059 + 'AlmanacManageClusterServicesCapability' => 'PhabricatorPolicyCapability', 4061 4060 'AlmanacManagementRegisterWorkflow' => 'AlmanacManagementWorkflow', 4062 4061 'AlmanacManagementTrustKeyWorkflow' => 'AlmanacManagementWorkflow', 4063 - 'AlmanacManagementUnlockWorkflow' => 'AlmanacManagementWorkflow', 4064 4062 'AlmanacManagementUntrustKeyWorkflow' => 'AlmanacManagementWorkflow', 4065 4063 'AlmanacManagementWorkflow' => 'PhabricatorManagementWorkflow', 4066 4064 'AlmanacNames' => 'Phobject', ··· 4130 4128 'PhabricatorDestructibleInterface', 4131 4129 'PhabricatorNgramsInterface', 4132 4130 'PhabricatorConduitResultInterface', 4131 + 'PhabricatorExtendedPolicyInterface', 4133 4132 ), 4134 4133 'AlmanacServiceController' => 'AlmanacController', 4135 4134 'AlmanacServiceDatasource' => 'PhabricatorTypeaheadDatasource',
+1 -1
src/applications/almanac/application/PhabricatorAlmanacApplication.php
··· 93 93 AlmanacCreateNamespacesCapability::CAPABILITY => array( 94 94 'default' => PhabricatorPolicies::POLICY_ADMIN, 95 95 ), 96 - AlmanacCreateClusterServicesCapability::CAPABILITY => array( 96 + AlmanacManageClusterServicesCapability::CAPABILITY => array( 97 97 'default' => PhabricatorPolicies::POLICY_ADMIN, 98 98 ), 99 99 );
-17
src/applications/almanac/capability/AlmanacCreateClusterServicesCapability.php
··· 1 - <?php 2 - 3 - final class AlmanacCreateClusterServicesCapability 4 - extends PhabricatorPolicyCapability { 5 - 6 - const CAPABILITY = 'almanac.cluster'; 7 - 8 - public function getCapabilityName() { 9 - return pht('Can Create Cluster Services'); 10 - } 11 - 12 - public function describeCapabilityRejection() { 13 - return pht( 14 - 'You do not have permission to create Almanac cluster services.'); 15 - } 16 - 17 - }
+17
src/applications/almanac/capability/AlmanacManageClusterServicesCapability.php
··· 1 + <?php 2 + 3 + final class AlmanacManageClusterServicesCapability 4 + extends PhabricatorPolicyCapability { 5 + 6 + const CAPABILITY = 'almanac.cluster'; 7 + 8 + public function getCapabilityName() { 9 + return pht('Can Manage Cluster Services'); 10 + } 11 + 12 + public function describeCapabilityRejection() { 13 + return pht( 14 + 'You do not have permission to manage Almanac cluster services.'); 15 + } 16 + 17 + }
+5 -3
src/applications/almanac/controller/AlmanacBindingViewController.php
··· 39 39 ->setHeader($header) 40 40 ->addPropertyList($property_list); 41 41 42 - if ($binding->getService()->getIsLocked()) { 43 - $this->addLockMessage( 42 + if ($binding->getService()->isClusterService()) { 43 + $this->addClusterMessage( 44 44 $box, 45 + pht('The service for this binding is a cluster service.'), 45 46 pht( 46 - 'This service for this binding is locked, so the binding can '. 47 + 'The service for this binding is a cluster service. You do not '. 48 + 'have permission to manage cluster services, so this binding can '. 47 49 'not be edited.')); 48 50 } 49 51
+21 -3
src/applications/almanac/controller/AlmanacController.php
··· 166 166 ->setTable($table); 167 167 } 168 168 169 - protected function addLockMessage(PHUIObjectBoxView $box, $message) { 169 + protected function addClusterMessage( 170 + PHUIObjectBoxView $box, 171 + $positive, 172 + $negative) { 173 + 174 + $can_manage = $this->hasApplicationCapability( 175 + AlmanacManageClusterServicesCapability::CAPABILITY); 176 + 170 177 $doc_link = phutil_tag( 171 178 'a', 172 179 array( ··· 175 182 ), 176 183 pht('Learn More')); 177 184 185 + if ($can_manage) { 186 + $severity = PHUIInfoView::SEVERITY_NOTICE; 187 + $message = $positive; 188 + } else { 189 + $severity = PHUIInfoView::SEVERITY_WARNING; 190 + $message = $negative; 191 + } 192 + 193 + $icon = id(new PHUIIconView()) 194 + ->setIcon('fa-sitemap'); 195 + 178 196 $error_view = id(new PHUIInfoView()) 179 - ->setSeverity(PHUIInfoView::SEVERITY_WARNING) 197 + ->setSeverity($severity) 180 198 ->setErrors( 181 199 array( 182 - array($message, ' ', $doc_link), 200 + array($icon, ' ', $message, ' ', $doc_link), 183 201 )); 184 202 185 203 $box->setInfoView($error_view);
+10 -12
src/applications/almanac/controller/AlmanacDeviceViewController.php
··· 21 21 return new Aphront404Response(); 22 22 } 23 23 24 - // We rebuild locks on a device when viewing the detail page, so they 25 - // automatically get corrected if they fall out of sync. 26 - $device->rebuildDeviceLocks(); 27 - 28 24 $title = pht('Device %s', $device->getName()); 29 25 30 26 $property_list = $this->buildPropertyList($device); ··· 40 36 ->setHeader($header) 41 37 ->addPropertyList($property_list); 42 38 43 - if ($device->getIsLocked()) { 44 - $this->addLockMessage( 39 + if ($device->isClusterDevice()) { 40 + $this->addClusterMessage( 45 41 $box, 42 + pht('This device is bound to a cluster service.'), 46 43 pht( 47 - 'This device is bound to a locked service, so it can not be '. 48 - 'edited.')); 44 + 'This device is bound to a cluster service. You do not have '. 45 + 'permission to manage cluster services, so the device can not '. 46 + 'be edited.')); 49 47 } 50 48 51 49 $interfaces = $this->buildInterfaceList($device); ··· 219 217 220 218 $handles = $viewer->loadHandles(mpull($services, 'getPHID')); 221 219 222 - $icon_lock = id(new PHUIIconView()) 223 - ->setIcon('fa-lock'); 220 + $icon_cluster = id(new PHUIIconView()) 221 + ->setIcon('fa-sitemap'); 224 222 225 223 $rows = array(); 226 224 foreach ($services as $service) { 227 225 $rows[] = array( 228 - ($service->getIsLocked() 229 - ? $icon_lock 226 + ($service->isClusterService() 227 + ? $icon_cluster 230 228 : null), 231 229 $handles->renderHandle($service->getPHID()), 232 230 );
+2 -2
src/applications/almanac/controller/AlmanacServiceEditController.php
··· 43 43 $service_type = $service_types[$service_class]; 44 44 if ($service_type->isClusterServiceType()) { 45 45 $this->requireApplicationCapability( 46 - AlmanacCreateClusterServicesCapability::CAPABILITY); 46 + AlmanacManageClusterServicesCapability::CAPABILITY); 47 47 } 48 48 49 49 $service = AlmanacService::initializeNewService(); ··· 190 190 } 191 191 192 192 list($can_cluster, $cluster_link) = $this->explainApplicationCapability( 193 - AlmanacCreateClusterServicesCapability::CAPABILITY, 193 + AlmanacManageClusterServicesCapability::CAPABILITY, 194 194 pht('You have permission to create cluster services.'), 195 195 pht('You do not have permission to create new cluster services.')); 196 196
+6 -8
src/applications/almanac/controller/AlmanacServiceViewController.php
··· 36 36 ->setHeader($header) 37 37 ->addPropertyList($property_list); 38 38 39 - $messages = $service->getServiceType()->getStatusMessages($service); 40 - if ($messages) { 41 - $box->setFormErrors($messages); 42 - } 43 - 44 - if ($service->getIsLocked()) { 45 - $this->addLockMessage( 39 + if ($service->isClusterService()) { 40 + $this->addClusterMessage( 46 41 $box, 47 - pht('This service is locked, and can not be edited.')); 42 + pht('This is a cluster service.'), 43 + pht( 44 + 'This service is a cluster service. You do not have permission to '. 45 + 'edit cluster services, so you can not edit this service.')); 48 46 } 49 47 50 48 $bindings = $this->buildBindingList($service);
+30
src/applications/almanac/editor/AlmanacBindingEditor.php
··· 3 3 final class AlmanacBindingEditor 4 4 extends AlmanacEditor { 5 5 6 + private $devicePHID; 7 + 6 8 public function getEditorObjectsDescription() { 7 9 return pht('Almanac Binding'); 8 10 } ··· 62 64 63 65 switch ($xaction->getTransactionType()) { 64 66 case AlmanacBindingTransaction::TYPE_INTERFACE: 67 + $interface_phids = array(); 68 + 69 + $interface_phids[] = $xaction->getOldValue(); 70 + $interface_phids[] = $xaction->getNewValue(); 71 + 72 + $interface_phids = array_filter($interface_phids); 73 + $interface_phids = array_unique($interface_phids); 74 + 75 + $interfaces = id(new AlmanacInterfaceQuery()) 76 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 77 + ->withPHIDs($interface_phids) 78 + ->execute(); 79 + 80 + $device_phids = array(); 81 + foreach ($interfaces as $interface) { 82 + $device_phids[] = $interface->getDevicePHID(); 83 + } 84 + 85 + $device_phids = array_unique($device_phids); 86 + 87 + $devices = id(new AlmanacDeviceQuery()) 88 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 89 + ->withPHIDs($device_phids) 90 + ->execute(); 91 + 92 + foreach ($devices as $device) { 93 + $device->rebuildClusterBindingStatus(); 94 + } 65 95 return; 66 96 } 67 97
-25
src/applications/almanac/editor/AlmanacServiceEditor.php
··· 11 11 $types = parent::getTransactionTypes(); 12 12 13 13 $types[] = AlmanacServiceTransaction::TYPE_NAME; 14 - $types[] = AlmanacServiceTransaction::TYPE_LOCK; 15 14 16 15 $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; 17 16 $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; ··· 25 24 switch ($xaction->getTransactionType()) { 26 25 case AlmanacServiceTransaction::TYPE_NAME: 27 26 return $object->getName(); 28 - case AlmanacServiceTransaction::TYPE_LOCK: 29 - return (bool)$object->getIsLocked(); 30 27 } 31 28 32 29 return parent::getCustomTransactionOldValue($object, $xaction); ··· 39 36 switch ($xaction->getTransactionType()) { 40 37 case AlmanacServiceTransaction::TYPE_NAME: 41 38 return $xaction->getNewValue(); 42 - case AlmanacServiceTransaction::TYPE_LOCK: 43 - return (bool)$xaction->getNewValue(); 44 39 } 45 40 46 41 return parent::getCustomTransactionNewValue($object, $xaction); ··· 54 49 case AlmanacServiceTransaction::TYPE_NAME: 55 50 $object->setName($xaction->getNewValue()); 56 51 return; 57 - case AlmanacServiceTransaction::TYPE_LOCK: 58 - $object->setIsLocked((int)$xaction->getNewValue()); 59 - return; 60 52 } 61 53 62 54 return parent::applyCustomInternalTransaction($object, $xaction); ··· 68 60 69 61 switch ($xaction->getTransactionType()) { 70 62 case AlmanacServiceTransaction::TYPE_NAME: 71 - return; 72 - case AlmanacServiceTransaction::TYPE_LOCK: 73 - $service = id(new AlmanacServiceQuery()) 74 - ->setViewer(PhabricatorUser::getOmnipotentUser()) 75 - ->withPHIDs(array($object->getPHID())) 76 - ->needBindings(true) 77 - ->executeOne(); 78 - 79 - $devices = array(); 80 - foreach ($service->getBindings() as $binding) { 81 - $device = $binding->getInterface()->getDevice(); 82 - $devices[$device->getPHID()] = $device; 83 - } 84 - 85 - foreach ($devices as $device) { 86 - $device->rebuildDeviceLocks(); 87 - } 88 63 return; 89 64 } 90 65
-49
src/applications/almanac/management/AlmanacManagementLockWorkflow.php
··· 1 - <?php 2 - 3 - final class AlmanacManagementLockWorkflow 4 - extends AlmanacManagementWorkflow { 5 - 6 - protected 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 - protected 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 - }
+4
src/applications/almanac/query/AlmanacDeviceSearchEngine.php
··· 84 84 ->setHref($device->getURI()) 85 85 ->setObject($device); 86 86 87 + if ($device->isClusterDevice()) { 88 + $item->addIcon('fa-sitemap', pht('Cluster Device')); 89 + } 90 + 87 91 $list->addItem($item); 88 92 } 89 93
-13
src/applications/almanac/query/AlmanacServiceQuery.php
··· 8 8 private $names; 9 9 private $serviceClasses; 10 10 private $devicePHIDs; 11 - private $locked; 12 11 private $namePrefix; 13 12 private $nameSuffix; 14 13 ··· 36 35 37 36 public function withDevicePHIDs(array $phids) { 38 37 $this->devicePHIDs = $phids; 39 - return $this; 40 - } 41 - 42 - public function withLocked($locked) { 43 - $this->locked = $locked; 44 38 return $this; 45 39 } 46 40 ··· 127 121 $conn, 128 122 'binding.devicePHID IN (%Ls)', 129 123 $this->devicePHIDs); 130 - } 131 - 132 - if ($this->locked !== null) { 133 - $where[] = qsprintf( 134 - $conn, 135 - 'service.isLocked = %d', 136 - (int)$this->locked); 137 124 } 138 125 139 126 if ($this->namePrefix !== null) {
-9
src/applications/almanac/query/AlmanacServiceSearchEngine.php
··· 101 101 $service->getServiceType()->getServiceTypeIcon(), 102 102 $service->getServiceType()->getServiceTypeShortName()); 103 103 104 - if ($service->getIsLocked() || 105 - $service->getServiceType()->isClusterServiceType()) { 106 - if ($service->getIsLocked()) { 107 - $item->addIcon('fa-lock', pht('Locked')); 108 - } else { 109 - $item->addIcon('fa-unlock-alt red', pht('Unlocked')); 110 - } 111 - } 112 - 113 104 $list->addItem($item); 114 105 } 115 106
-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 - 38 14 }
-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 - 62 58 /** 63 59 * List all available service type implementations. 64 60 *
+22 -8
src/applications/almanac/storage/AlmanacBinding.php
··· 6 6 PhabricatorPolicyInterface, 7 7 PhabricatorApplicationTransactionInterface, 8 8 AlmanacPropertyInterface, 9 - PhabricatorDestructibleInterface { 9 + PhabricatorDestructibleInterface, 10 + PhabricatorExtendedPolicyInterface { 10 11 11 12 protected $servicePHID; 12 13 protected $devicePHID; ··· 157 158 'interface.'), 158 159 ); 159 160 160 - if ($capability === PhabricatorPolicyCapability::CAN_EDIT) { 161 - if ($this->getService()->getIsLocked()) { 162 - $notes[] = pht( 163 - 'The service for this binding is locked, so it can not be edited.'); 164 - } 161 + return $notes; 162 + } 163 + 164 + 165 + /* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ 166 + 167 + 168 + public function getExtendedPolicy($capability, PhabricatorUser $viewer) { 169 + switch ($capability) { 170 + case PhabricatorPolicyCapability::CAN_EDIT: 171 + if ($this->getService()->isClusterService()) { 172 + return array( 173 + array( 174 + new PhabricatorAlmanacApplication(), 175 + AlmanacManageClusterServicesCapability::CAPABILITY, 176 + ), 177 + ); 178 + } 179 + break; 165 180 } 166 181 167 - return $notes; 182 + return array(); 168 183 } 169 - 170 184 171 185 /* -( PhabricatorApplicationTransactionInterface )------------------------- */ 172 186
+42 -30
src/applications/almanac/storage/AlmanacDevice.php
··· 10 10 AlmanacPropertyInterface, 11 11 PhabricatorDestructibleInterface, 12 12 PhabricatorNgramsInterface, 13 - PhabricatorConduitResultInterface { 13 + PhabricatorConduitResultInterface, 14 + PhabricatorExtendedPolicyInterface { 14 15 15 16 protected $name; 16 17 protected $nameIndex; 17 18 protected $mailKey; 18 19 protected $viewPolicy; 19 20 protected $editPolicy; 20 - protected $isLocked; 21 + protected $isBoundToClusterService; 21 22 22 23 private $almanacProperties = self::ATTACHABLE; 23 24 ··· 26 27 ->setViewPolicy(PhabricatorPolicies::POLICY_USER) 27 28 ->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN) 28 29 ->attachAlmanacProperties(array()) 29 - ->setIsLocked(0); 30 + ->setIsBoundToClusterService(0); 30 31 } 31 32 32 33 protected function getConfiguration() { ··· 36 37 'name' => 'text128', 37 38 'nameIndex' => 'bytes12', 38 39 'mailKey' => 'bytes20', 39 - 'isLocked' => 'bool', 40 + 'isBoundToClusterService' => 'bool', 40 41 ), 41 42 self::CONFIG_KEY_SCHEMA => array( 42 43 'key_name' => array( ··· 70 71 return '/almanac/device/view/'.$this->getName().'/'; 71 72 } 72 73 73 - 74 - /** 75 - * Find locked services which are bound to this device, updating the device 76 - * lock flag if necessary. 77 - * 78 - * @return list<phid> List of locking service PHIDs. 79 - */ 80 - public function rebuildDeviceLocks() { 74 + public function rebuildClusterBindingStatus() { 81 75 $services = id(new AlmanacServiceQuery()) 82 76 ->setViewer(PhabricatorUser::getOmnipotentUser()) 83 77 ->withDevicePHIDs(array($this->getPHID())) 84 - ->withLocked(true) 85 78 ->execute(); 86 79 87 - $locked = (bool)count($services); 80 + $is_cluster = false; 81 + foreach ($services as $service) { 82 + if ($service->isClusterService()) { 83 + $is_cluster = true; 84 + break; 85 + } 86 + } 88 87 89 - if ($locked != $this->getIsLocked()) { 90 - $this->setIsLocked((int)$locked); 88 + if ($is_cluster != $this->getIsBoundToClusterService()) { 89 + $this->setIsBoundToClusterService((int)$is_cluster); 91 90 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 92 91 queryfx( 93 92 $this->establishConnection('w'), 94 - 'UPDATE %T SET isLocked = %d WHERE id = %d', 93 + 'UPDATE %T SET isBoundToClusterService = %d WHERE id = %d', 95 94 $this->getTableName(), 96 - $this->getIsLocked(), 95 + $this->getIsBoundToClusterService(), 97 96 $this->getID()); 98 97 unset($unguarded); 99 98 } ··· 101 100 return $this; 102 101 } 103 102 103 + public function isClusterDevice() { 104 + return $this->getIsBoundToClusterService(); 105 + } 106 + 104 107 105 108 /* -( AlmanacPropertyInterface )------------------------------------------- */ 106 109 ··· 156 159 case PhabricatorPolicyCapability::CAN_VIEW: 157 160 return $this->getViewPolicy(); 158 161 case PhabricatorPolicyCapability::CAN_EDIT: 159 - if ($this->getIsLocked()) { 160 - return PhabricatorPolicies::POLICY_NOONE; 161 - } else { 162 - return $this->getEditPolicy(); 163 - } 162 + return $this->getEditPolicy(); 164 163 } 165 164 } 166 165 ··· 169 168 } 170 169 171 170 public function describeAutomaticCapability($capability) { 172 - if ($capability === PhabricatorPolicyCapability::CAN_EDIT) { 173 - if ($this->getIsLocked()) { 174 - return pht( 175 - 'This device is bound to a locked service, so it can not '. 176 - 'be edited.'); 177 - } 171 + return null; 172 + } 173 + 174 + 175 + /* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ 176 + 177 + 178 + public function getExtendedPolicy($capability, PhabricatorUser $viewer) { 179 + switch ($capability) { 180 + case PhabricatorPolicyCapability::CAN_EDIT: 181 + if ($this->isClusterDevice()) { 182 + return array( 183 + array( 184 + new PhabricatorAlmanacApplication(), 185 + AlmanacManageClusterServicesCapability::CAPABILITY, 186 + ), 187 + ); 188 + } 189 + break; 178 190 } 179 191 180 - return null; 192 + return array(); 181 193 } 182 194 183 195
-7
src/applications/almanac/storage/AlmanacInterface.php
··· 101 101 'view the interface.'), 102 102 ); 103 103 104 - if ($capability === PhabricatorPolicyCapability::CAN_EDIT) { 105 - if ($this->getDevice()->getIsLocked()) { 106 - $notes[] = pht( 107 - 'The device for this interface is locked, so it can not be edited.'); 108 - } 109 - } 110 - 111 104 return $notes; 112 105 } 113 106
+24 -13
src/applications/almanac/storage/AlmanacService.php
··· 9 9 AlmanacPropertyInterface, 10 10 PhabricatorDestructibleInterface, 11 11 PhabricatorNgramsInterface, 12 - PhabricatorConduitResultInterface { 12 + PhabricatorConduitResultInterface, 13 + PhabricatorExtendedPolicyInterface { 13 14 14 15 protected $name; 15 16 protected $nameIndex; ··· 17 18 protected $viewPolicy; 18 19 protected $editPolicy; 19 20 protected $serviceClass; 20 - protected $isLocked; 21 21 22 22 private $almanacProperties = self::ATTACHABLE; 23 23 private $bindings = self::ATTACHABLE; ··· 27 27 return id(new AlmanacService()) 28 28 ->setViewPolicy(PhabricatorPolicies::POLICY_USER) 29 29 ->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN) 30 - ->attachAlmanacProperties(array()) 31 - ->setIsLocked(0); 30 + ->attachAlmanacProperties(array()); 32 31 } 33 32 34 33 protected function getConfiguration() { ··· 39 38 'nameIndex' => 'bytes12', 40 39 'mailKey' => 'bytes20', 41 40 'serviceClass' => 'text64', 42 - 'isLocked' => 'bool', 43 41 ), 44 42 self::CONFIG_KEY_SCHEMA => array( 45 43 'key_name' => array( ··· 94 92 return $this; 95 93 } 96 94 95 + public function isClusterService() { 96 + return $this->getServiceType()->isClusterServiceType(); 97 + } 98 + 97 99 98 100 /* -( AlmanacPropertyInterface )------------------------------------------- */ 99 101 ··· 149 151 case PhabricatorPolicyCapability::CAN_VIEW: 150 152 return $this->getViewPolicy(); 151 153 case PhabricatorPolicyCapability::CAN_EDIT: 152 - if ($this->getIsLocked()) { 153 - return PhabricatorPolicies::POLICY_NOONE; 154 - } else { 155 - return $this->getEditPolicy(); 156 - } 154 + return $this->getEditPolicy(); 157 155 } 158 156 } 159 157 ··· 162 160 } 163 161 164 162 public function describeAutomaticCapability($capability) { 163 + return null; 164 + } 165 + 166 + 167 + /* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ 168 + 169 + 170 + public function getExtendedPolicy($capability, PhabricatorUser $viewer) { 165 171 switch ($capability) { 166 172 case PhabricatorPolicyCapability::CAN_EDIT: 167 - if ($this->getIsLocked()) { 168 - return pht('This service is locked and can not be edited.'); 173 + if ($this->isClusterService()) { 174 + return array( 175 + array( 176 + new PhabricatorAlmanacApplication(), 177 + AlmanacManageClusterServicesCapability::CAPABILITY, 178 + ), 179 + ); 169 180 } 170 181 break; 171 182 } 172 183 173 - return null; 184 + return array(); 174 185 } 175 186 176 187
+7 -4
src/applications/policy/filter/PhabricatorPolicyFilter.php
··· 381 381 $reject = $extended_objects[$extended_key]; 382 382 unset($extended_objects[$extended_key]); 383 383 384 - // TODO: This isn't as user-friendly as it could be. It's possible 385 - // that we're rejecting this object for multiple capability/policy 386 - // failures, though. 387 - $this->rejectObject($reject, false, '<extended>'); 384 + // It's possible that we're rejecting this object for multiple 385 + // capability/policy failures, but just pick the first one to show 386 + // to the user. 387 + $first_capability = head($capabilities); 388 + $first_policy = $object_in->getPolicy($first_capability); 389 + 390 + $this->rejectObject($reject, $first_policy, $first_capability); 388 391 } 389 392 } 390 393 }
+3 -16
src/docs/user/configuration/cluster.diviner
··· 12 12 Locking Services 13 13 ================ 14 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. 15 + Very briefly, you can set "Can Manage Cluster Services" to "No One" to lock 16 + the cluster definition. 19 17 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}. 18 + See also @{article:Almanac User Guide}.
-32
src/docs/user/userguide/almanac.diviner
··· 177 177 permission on the service or device itself, as long as they don't try to rename 178 178 the service or device to move it into a namespace they don't have permission 179 179 to access. 180 - 181 - 182 - Locking and Unlocking Services 183 - ============================== 184 - 185 - Services can be locked to prevent edits from the web UI. This primarily hardens 186 - Almanac against attacks involving account compromise. Notably, locking cluster 187 - services prevents an attacker from modifying the Phabricator cluster definition. 188 - For more details on this scenario, see 189 - @{article:User Guide: Phabricator Clusters}. 190 - 191 - Beyond hardening cluster definitions, you might also want to lock a critical 192 - service to prevent accidental edits. 193 - 194 - To lock a service, run: 195 - 196 - phabricator/ $ ./bin/almanac lock <service> 197 - 198 - To unlock a service later, run: 199 - 200 - phabricator/ $ ./bin/almanac unlock <service> 201 - 202 - Locking a service also locks all of the service's bindings and properties, as 203 - well as the devices connected to the service. Generally, no part of the 204 - service definition can be modified while it is locked. 205 - 206 - Devices (and their properties) will remain locked as long as they are bound to 207 - at least one locked service. To edit a device, you'll need to unlock all the 208 - services it is bound to. 209 - 210 - Locked services and devices will show that they are locked in the web UI, and 211 - editing options will be unavailable.