diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4079,7 +4079,7 @@ 'AlmanacNamespaceListController' => 'AlmanacNamespaceController', 'AlmanacNamespaceNameNgrams' => 'PhabricatorSearchNgrams', 'AlmanacNamespacePHIDType' => 'PhabricatorPHIDType', - 'AlmanacNamespaceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'AlmanacNamespaceQuery' => 'AlmanacQuery', 'AlmanacNamespaceSearchEngine' => 'PhabricatorApplicationSearchEngine', 'AlmanacNamespaceTransaction' => 'PhabricatorApplicationTransaction', 'AlmanacNamespaceTransactionQuery' => 'PhabricatorApplicationTransactionQuery', diff --git a/src/applications/almanac/editor/AlmanacDeviceEditor.php b/src/applications/almanac/editor/AlmanacDeviceEditor.php --- a/src/applications/almanac/editor/AlmanacDeviceEditor.php +++ b/src/applications/almanac/editor/AlmanacDeviceEditor.php @@ -148,22 +148,42 @@ $message, $xaction); $errors[] = $error; + continue; } - } - } - if ($xactions) { - $duplicate = id(new AlmanacDeviceQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withNames(array(last($xactions)->getNewValue())) - ->executeOne(); - if ($duplicate && ($duplicate->getID() != $object->getID())) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Not Unique'), - pht('Almanac devices must have unique names.'), - last($xactions)); - $errors[] = $error; + $other = id(new AlmanacDeviceQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withNames(array($name)) + ->executeOne(); + if ($other && ($other->getID() != $object->getID())) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Not Unique'), + pht('Almanac devices must have unique names.'), + $xaction); + $errors[] = $error; + continue; + } + + if ($name === $object->getName()) { + continue; + } + + $namespace = AlmanacNamespace::loadRestrictedNamespace( + $this->getActor(), + $name); + if ($namespace) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Restricted'), + pht( + 'You do not have permission to create Almanac devices '. + 'within the "%s" namespace.', + $namespace->getName()), + $xaction); + $errors[] = $error; + continue; + } } } diff --git a/src/applications/almanac/editor/AlmanacNamespaceEditor.php b/src/applications/almanac/editor/AlmanacNamespaceEditor.php --- a/src/applications/almanac/editor/AlmanacNamespaceEditor.php +++ b/src/applications/almanac/editor/AlmanacNamespaceEditor.php @@ -123,7 +123,7 @@ if ($other && ($other->getID() != $object->getID())) { $error = new PhabricatorApplicationTransactionValidationError( $type, - pht('Invalid'), + pht('Not Unique'), pht( 'The namespace name "%s" is already in use by another '. 'namespace. Each namespace must have a unique name.', @@ -132,6 +132,26 @@ $errors[] = $error; continue; } + + if ($name === $object->getName()) { + continue; + } + + $namespace = AlmanacNamespace::loadRestrictedNamespace( + $this->getActor(), + $name); + if ($namespace) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Restricted'), + pht( + 'You do not have permission to create Almanac namespaces '. + 'within the "%s" namespace.', + $namespace->getName()), + $xaction); + $errors[] = $error; + continue; + } } } diff --git a/src/applications/almanac/editor/AlmanacServiceEditor.php b/src/applications/almanac/editor/AlmanacServiceEditor.php --- a/src/applications/almanac/editor/AlmanacServiceEditor.php +++ b/src/applications/almanac/editor/AlmanacServiceEditor.php @@ -140,22 +140,42 @@ $message, $xaction); $errors[] = $error; + continue; + } + + $other = id(new AlmanacServiceQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withNames(array($name)) + ->executeOne(); + if ($other && ($other->getID() != $object->getID())) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Not Unique'), + pht('Almanac services must have unique names.'), + last($xactions)); + $errors[] = $error; + continue; } - } - } - if ($xactions) { - $duplicate = id(new AlmanacServiceQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withNames(array(last($xactions)->getNewValue())) - ->executeOne(); - if ($duplicate && ($duplicate->getID() != $object->getID())) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Not Unique'), - pht('Almanac services must have unique names.'), - last($xactions)); - $errors[] = $error; + if ($name === $object->getName()) { + continue; + } + + $namespace = AlmanacNamespace::loadRestrictedNamespace( + $this->getActor(), + $name); + if ($namespace) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Restricted'), + pht( + 'You do not have permission to create Almanac services '. + 'within the "%s" namespace.', + $namespace->getName()), + $xaction); + $errors[] = $error; + continue; + } } } diff --git a/src/applications/almanac/query/AlmanacNamespaceQuery.php b/src/applications/almanac/query/AlmanacNamespaceQuery.php --- a/src/applications/almanac/query/AlmanacNamespaceQuery.php +++ b/src/applications/almanac/query/AlmanacNamespaceQuery.php @@ -1,7 +1,7 @@ getName().'/'; } + public function getNameLength() { + return strlen($this->getName()); + } + + /** + * Load the namespace which prevents use of an Almanac name, if one exists. + */ + public static function loadRestrictedNamespace( + PhabricatorUser $viewer, + $name) { + + // For a name like "x.y.z", produce a list of controlling namespaces like + // ("z", "y.x", "x.y.z"). + $names = array(); + $parts = explode('.', $name); + for ($ii = 0; $ii < count($parts); $ii++) { + $names[] = implode('.', array_slice($parts, -($ii + 1))); + } + + // Load all the possible controlling namespaces. + $namespaces = id(new AlmanacNamespaceQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withNames($names) + ->execute(); + if (!$namespaces) { + return null; + } + + // Find the "nearest" (longest) namespace that exists. If both + // "sub.domain.com" and "domain.com" exist, we only care about the policy + // on the former. + $namespaces = msort($namespaces, 'getNameLength'); + $namespace = last($namespaces); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $namespace, + PhabricatorPolicyCapability::CAN_EDIT); + if ($can_edit) { + return null; + } + + return $namespace; + } + /* -( AlmanacPropertyInterface )------------------------------------------- */ diff --git a/src/docs/user/userguide/almanac.diviner b/src/docs/user/userguide/almanac.diviner --- a/src/docs/user/userguide/almanac.diviner +++ b/src/docs/user/userguide/almanac.diviner @@ -133,6 +133,52 @@ `b.mycompany.com`. +Namespaces +========== + +Almanac namespaces allow you to control who can create services and devices +with certain names. + +If you keep a list of cattle as devices with names like +`cow001.herd.myranch.com`, `cow002.herd.myranch.moo`, you might have some +applications which query for all devices in `*.herd.myranch.moo`, and thus +want to limit who can create devices there in order to prevent mistakes. + +If a namespace like `herd.myranch.moo` exists, users must have permission to +edit the namespace in order to create new services, devices, or namespaces +within it. For example, a user can not create `cow003.herd.myranch.moo` if +they do not have edit permission on the `herd.myranch.moo` namespace. + +When you try to create a `cow003.herd.myranch.moo` service (or rename an +existing service to have that name), Almanac looks for these namespaces, then +checks the policy of the first one it finds: + +| Namespace | +|----|----- +| `cow003.herd.ranch.moo` | //"Nearest" namespace, considered first.// +| `herd.ranch.moo` | | +| `ranch.moo` | | +| `moo` | //"Farthest" namespace, considered last.// + +Note that namespaces treat names as lists of domain parts, not as strict +substrings, so the namespace `herd.myranch.moo` does not prevent +someone from creating `goatherd.myranch.moo` or `goat001.goatherd.myranch.moo`. +The name `goatherd.myranch.moo` is not part of the `herd.myranch.moo` namespace +because the initial subdomain differs. + +If a name belongs to multiple namespaces, the policy of the nearest namespace +is controlling. For example, if `myranch.moo` has a very restrictive edit +policy but `shed.myranch.moo` has a more open one, users can create devices and +services like `rake.shed.myranch.moo` as long as they can pass the policy check +for `shed.myranch.moo`, even if they do not have permission under the policy +for `myranch.moo`. + +Users can edit services and devices within a namespace if they have edit +permission on the service or device itself, as long as they don't try to rename +the service or device to move it into a namespace they don't have permission +to access. + + Locking and Unlocking Services ==============================