From 2f6d3119f5f6f022bae2a432c6691ec4f14f750d Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 10 Oct 2015 07:15:25 -0700 Subject: [PATCH] Rough cut of "Blueprint Authorizations" Summary: Ref T9519. This is like 80% of the way there and doesn't fully work yet, but roughly shows the shape of things to come. Here's how it works: First, there's a new custom field type for blueprints which works like a normal typeahead but has some extra logic. It's implemented this way to make it easy to add to Blueprints in Drydock and Build Plans in Harbormaster. Here, I've added a "Use Blueprints" field to the "WorkingCopy" blueprint, so you can control which hosts the working copies are permitted to allocate on: {F869865} This control has a bit of custom rendering logic. Instead of rendering a normal list of PHIDs, it renders an annotated list with icons: {F869866} These icons show whether the blueprint on the other size of the authorization has approved this object. Once you have a green checkmark, you're good to go. On the blueprint side, things look like this: {F869867} This table shows all the objects which have asked for access to this blueprint. In this case it's showing that one object is approved to use the blueprint since I already approved it, but by default new requests come in here as "Authorization Requested" and someone has to go approve them. You approve them from within the authorization detail screen: {F869868} You can use the "Approve" or "Decline" buttons to allow or prevent use of the blueprint. This doesn't actually do anything yet -- objects don't need to be authorized in order to use blueprints quite yet. That will come in the next diff, I just wanted to get the UI in reasonable shape first. The authorization also has a second piece of state, which is whether the request from the object is active or inactive. We use this to keep track of the authorization if the blueprint is (maybe temporarily) deleted. For example, you might have a Build Plan that uses Blueprints A and B. For a couple days, you only want to use A, so you remove B from the "Use Blueprints: ..." field. Later, you can add B back and it will connect to its old authorization again, so you don't need to go re-approve things (and if you're declined, you stay declined instead of being able to request authorization over and over again). This should make working with authorizations a little easier and less labor intensive. Stuff not in this diff: - Actually preventing any allocations (next diff). - Probably should have transactions for approve/decline, at least, at some point, so there's a log of who did approvals and when. - Maybe should have a more clear/loud error state when no blueprints are approved? - Should probably restrict the typeahead to specific blueprint types. Test Plan: - Added the field. - Typed some stuff into it. - Saw the UI update properly. - Approved an authorization. - Declined an authorization. - Saw active authorizations on a blueprint page. - Didn't see any inactive authroizations there. - Clicked "View All Authorizations", saw all authorizations. Reviewers: chad, hach-que Reviewed By: chad Maniphest Tasks: T9519 Differential Revision: https://secure.phabricator.com/D14251 --- .../autopatches/20151009.drydock.auth.1.sql | 14 ++ src/__phutil_library_map__.php | 21 +++ .../PhabricatorDrydockApplication.php | 9 ++ ...dockWorkingCopyBlueprintImplementation.php | 10 ++ ...rydockAuthorizationAuthorizeController.php | 95 +++++++++++ .../DrydockAuthorizationListController.php | 87 +++++++++++ .../DrydockAuthorizationViewController.php | 141 +++++++++++++++++ .../DrydockBlueprintViewController.php | 71 ++++++++- .../drydock/controller/DrydockController.php | 2 +- .../DrydockResourceViewController.php | 2 +- .../DrydockBlueprintCoreCustomField.php | 5 - .../phid/DrydockAuthorizationPHIDType.php | 37 +++++ .../drydock/phid/DrydockBlueprintPHIDType.php | 7 +- .../query/DrydockAuthorizationQuery.php | 146 +++++++++++++++++ .../DrydockAuthorizationSearchEngine.php | 87 +++++++++++ .../drydock/storage/DrydockAuthorization.php | 122 +++++++++++++++ .../view/DrydockAuthorizationListView.php | 65 ++++++++ ...abricatorStandardCustomFieldBlueprints.php | 147 ++++++++++++++++++ .../PhabricatorStandardCustomFieldPHIDs.php | 2 +- 19 files changed, 1059 insertions(+), 11 deletions(-) create mode 100644 resources/sql/autopatches/20151009.drydock.auth.1.sql create mode 100644 src/applications/drydock/controller/DrydockAuthorizationAuthorizeController.php create mode 100644 src/applications/drydock/controller/DrydockAuthorizationListController.php create mode 100644 src/applications/drydock/controller/DrydockAuthorizationViewController.php create mode 100644 src/applications/drydock/phid/DrydockAuthorizationPHIDType.php create mode 100644 src/applications/drydock/query/DrydockAuthorizationQuery.php create mode 100644 src/applications/drydock/query/DrydockAuthorizationSearchEngine.php create mode 100644 src/applications/drydock/storage/DrydockAuthorization.php create mode 100644 src/applications/drydock/view/DrydockAuthorizationListView.php create mode 100644 src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldBlueprints.php diff --git a/resources/sql/autopatches/20151009.drydock.auth.1.sql b/resources/sql/autopatches/20151009.drydock.auth.1.sql new file mode 100644 index 0000000000..8e68977492 --- /dev/null +++ b/resources/sql/autopatches/20151009.drydock.auth.1.sql @@ -0,0 +1,14 @@ +CREATE TABLE {$NAMESPACE}_drydock.drydock_authorization ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + blueprintPHID VARBINARY(64) NOT NULL, + blueprintAuthorizationState VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT}, + objectPHID VARBINARY(64) NOT NULL, + objectAuthorizationState VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT}, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (phid), + UNIQUE KEY `key_unique` (objectPHID, blueprintPHID), + KEY `key_blueprint` (blueprintPHID, blueprintAuthorizationState), + KEY `key_object` (objectPHID, objectAuthorizationState) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index aaa4a1c5ac..ae2187265f 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -799,6 +799,14 @@ phutil_register_library_map(array( 'DoorkeeperTagsController' => 'applications/doorkeeper/controller/DoorkeeperTagsController.php', 'DrydockAlmanacServiceHostBlueprintImplementation' => 'applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php', 'DrydockApacheWebrootInterface' => 'applications/drydock/interface/webroot/DrydockApacheWebrootInterface.php', + 'DrydockAuthorization' => 'applications/drydock/storage/DrydockAuthorization.php', + 'DrydockAuthorizationAuthorizeController' => 'applications/drydock/controller/DrydockAuthorizationAuthorizeController.php', + 'DrydockAuthorizationListController' => 'applications/drydock/controller/DrydockAuthorizationListController.php', + 'DrydockAuthorizationListView' => 'applications/drydock/view/DrydockAuthorizationListView.php', + 'DrydockAuthorizationPHIDType' => 'applications/drydock/phid/DrydockAuthorizationPHIDType.php', + 'DrydockAuthorizationQuery' => 'applications/drydock/query/DrydockAuthorizationQuery.php', + 'DrydockAuthorizationSearchEngine' => 'applications/drydock/query/DrydockAuthorizationSearchEngine.php', + 'DrydockAuthorizationViewController' => 'applications/drydock/controller/DrydockAuthorizationViewController.php', 'DrydockBlueprint' => 'applications/drydock/storage/DrydockBlueprint.php', 'DrydockBlueprintController' => 'applications/drydock/controller/DrydockBlueprintController.php', 'DrydockBlueprintCoreCustomField' => 'applications/drydock/customfield/DrydockBlueprintCoreCustomField.php', @@ -2946,6 +2954,7 @@ phutil_register_library_map(array( 'PhabricatorSpacesTestCase' => 'applications/spaces/__tests__/PhabricatorSpacesTestCase.php', 'PhabricatorSpacesViewController' => 'applications/spaces/controller/PhabricatorSpacesViewController.php', 'PhabricatorStandardCustomField' => 'infrastructure/customfield/standard/PhabricatorStandardCustomField.php', + 'PhabricatorStandardCustomFieldBlueprints' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldBlueprints.php', 'PhabricatorStandardCustomFieldBool' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldBool.php', 'PhabricatorStandardCustomFieldCredential' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldCredential.php', 'PhabricatorStandardCustomFieldDatasource' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldDatasource.php', @@ -4537,6 +4546,17 @@ phutil_register_library_map(array( 'DoorkeeperTagsController' => 'PhabricatorController', 'DrydockAlmanacServiceHostBlueprintImplementation' => 'DrydockBlueprintImplementation', 'DrydockApacheWebrootInterface' => 'DrydockWebrootInterface', + 'DrydockAuthorization' => array( + 'DrydockDAO', + 'PhabricatorPolicyInterface', + ), + 'DrydockAuthorizationAuthorizeController' => 'DrydockController', + 'DrydockAuthorizationListController' => 'DrydockController', + 'DrydockAuthorizationListView' => 'AphrontView', + 'DrydockAuthorizationPHIDType' => 'PhabricatorPHIDType', + 'DrydockAuthorizationQuery' => 'DrydockQuery', + 'DrydockAuthorizationSearchEngine' => 'PhabricatorApplicationSearchEngine', + 'DrydockAuthorizationViewController' => 'DrydockController', 'DrydockBlueprint' => array( 'DrydockDAO', 'PhabricatorApplicationTransactionInterface', @@ -7091,6 +7111,7 @@ phutil_register_library_map(array( 'PhabricatorSpacesTestCase' => 'PhabricatorTestCase', 'PhabricatorSpacesViewController' => 'PhabricatorSpacesController', 'PhabricatorStandardCustomField' => 'PhabricatorCustomField', + 'PhabricatorStandardCustomFieldBlueprints' => 'PhabricatorStandardCustomFieldTokenizer', 'PhabricatorStandardCustomFieldBool' => 'PhabricatorStandardCustomField', 'PhabricatorStandardCustomFieldCredential' => 'PhabricatorStandardCustomField', 'PhabricatorStandardCustomFieldDatasource' => 'PhabricatorStandardCustomFieldTokenizer', diff --git a/src/applications/drydock/application/PhabricatorDrydockApplication.php b/src/applications/drydock/application/PhabricatorDrydockApplication.php index e662fea9e6..929b4658ed 100644 --- a/src/applications/drydock/application/PhabricatorDrydockApplication.php +++ b/src/applications/drydock/application/PhabricatorDrydockApplication.php @@ -57,6 +57,8 @@ final class PhabricatorDrydockApplication extends PhabricatorApplication { 'DrydockResourceListController', 'logs/(?:query/(?P[^/]+)/)?' => 'DrydockLogListController', + 'authorizations/(?:query/(?P[^/]+)/)?' => + 'DrydockAuthorizationListController', ), 'create/' => 'DrydockBlueprintCreateController', 'edit/(?:(?P[1-9]\d*)/)?' => 'DrydockBlueprintEditController', @@ -81,6 +83,13 @@ final class PhabricatorDrydockApplication extends PhabricatorApplication { 'DrydockLogListController', ), ), + '(?Pauthorization)/' => array( + '(?P[1-9]\d*)/' => array( + '' => 'DrydockAuthorizationViewController', + '(?Pauthorize|decline)/' => + 'DrydockAuthorizationAuthorizeController', + ), + ), ), ); } diff --git a/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php index 30dd6fe4c5..939d6d2e9b 100644 --- a/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php +++ b/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php @@ -390,5 +390,15 @@ final class DrydockWorkingCopyBlueprintImplementation return $lease; } + public function getFieldSpecifications() { + return array( + 'blueprintPHIDs' => array( + 'name' => pht('Use Blueprints'), + 'type' => 'blueprints', + 'required' => true, + ), + ) + parent::getFieldSpecifications(); + } + } diff --git a/src/applications/drydock/controller/DrydockAuthorizationAuthorizeController.php b/src/applications/drydock/controller/DrydockAuthorizationAuthorizeController.php new file mode 100644 index 0000000000..41010ff882 --- /dev/null +++ b/src/applications/drydock/controller/DrydockAuthorizationAuthorizeController.php @@ -0,0 +1,95 @@ +getViewer(); + $id = $request->getURIData('id'); + + $authorization = id(new DrydockAuthorizationQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$authorization) { + return new Aphront404Response(); + } + + $authorization_uri = $this->getApplicationURI("authorization/{$id}/"); + $is_authorize = ($request->getURIData('action') == 'authorize'); + + $state_authorized = DrydockAuthorization::BLUEPRINTAUTH_AUTHORIZED; + $state_declined = DrydockAuthorization::BLUEPRINTAUTH_DECLINED; + + $state = $authorization->getBlueprintAuthorizationState(); + $can_authorize = ($state != $state_authorized); + $can_decline = ($state != $state_declined); + + if ($is_authorize && !$can_authorize) { + return $this->newDialog() + ->setTitle(pht('Already Authorized')) + ->appendParagraph( + pht( + 'This authorization has already been approved.')) + ->addCancelButton($authorization_uri); + } + + if (!$is_authorize && !$can_decline) { + return $this->newDialog() + ->setTitle(pht('Already Declined')) + ->appendParagraph( + pht('This authorization has already been declined.')) + ->addCancelButton($authorization_uri); + } + + if ($request->isFormPost()) { + if ($is_authorize) { + $new_state = $state_authorized; + } else { + $new_state = $state_declined; + } + + $authorization + ->setBlueprintAuthorizationState($new_state) + ->save(); + + return id(new AphrontRedirectResponse())->setURI($authorization_uri); + } + + if ($is_authorize) { + $title = pht('Approve Authorization'); + $body = pht( + 'Approve this authorization? The object will be able to lease and '. + 'allocate resources created by this blueprint.'); + $button = pht('Approve Authorization'); + } else { + $title = pht('Decline Authorization'); + $body = pht( + 'Decline this authorization? The object will not be able to lease '. + 'or allocate resources created by this blueprint.'); + $button = pht('Decline Authorization'); + } + + return $this->newDialog() + ->setTitle($title) + ->appendParagraph($body) + ->addSubmitButton($button) + ->addCancelButton($authorization_uri); + } + + public function buildSideNavView() { + // TODO: Get rid of this, but it's currently required by DrydockController. + return null; + } + + public function buildApplicationMenu() { + // TODO: As above. + return null; + } + +} diff --git a/src/applications/drydock/controller/DrydockAuthorizationListController.php b/src/applications/drydock/controller/DrydockAuthorizationListController.php new file mode 100644 index 0000000000..164ca8e5cc --- /dev/null +++ b/src/applications/drydock/controller/DrydockAuthorizationListController.php @@ -0,0 +1,87 @@ +blueprint = $blueprint; + return $this; + } + + public function getBlueprint() { + return $this->blueprint; + } + + public function shouldAllowPublic() { + return true; + } + + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); + + $engine = new DrydockAuthorizationSearchEngine(); + + $id = $request->getURIData('id'); + + $blueprint = id(new DrydockBlueprintQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->executeOne(); + if (!$blueprint) { + return new Aphront404Response(); + } + + $this->setBlueprint($blueprint); + $engine->setBlueprint($blueprint); + + $querykey = $request->getURIData('queryKey'); + + $controller = id(new PhabricatorApplicationSearchController()) + ->setQueryKey($querykey) + ->setSearchEngine($engine) + ->setNavigation($this->buildSideNavView()); + + return $this->delegateToController($controller); + } + + public function buildSideNavView() { + $nav = new AphrontSideNavFilterView(); + $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); + + $engine = id(new DrydockAuthorizationSearchEngine()) + ->setViewer($this->getViewer()); + + $engine->setBlueprint($this->getBlueprint()); + $engine->addNavigationItems($nav->getMenu()); + + $nav->selectFilter(null); + + return $nav; + } + + protected function buildApplicationCrumbs() { + $crumbs = parent::buildApplicationCrumbs(); + + $blueprint = $this->getBlueprint(); + if ($blueprint) { + $id = $blueprint->getID(); + + $crumbs->addTextCrumb( + pht('Blueprints'), + $this->getApplicationURI('blueprint/')); + + $crumbs->addTextCrumb( + $blueprint->getBlueprintName(), + $this->getApplicationURI("blueprint/{$id}/")); + + $crumbs->addTextCrumb( + pht('Authorizations'), + $this->getApplicationURI("blueprint/{$id}/authorizations/")); + } + + return $crumbs; + } + +} diff --git a/src/applications/drydock/controller/DrydockAuthorizationViewController.php b/src/applications/drydock/controller/DrydockAuthorizationViewController.php new file mode 100644 index 0000000000..95270c4b51 --- /dev/null +++ b/src/applications/drydock/controller/DrydockAuthorizationViewController.php @@ -0,0 +1,141 @@ +getViewer(); + $id = $request->getURIData('id'); + + $authorization = id(new DrydockAuthorizationQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->executeOne(); + if (!$authorization) { + return new Aphront404Response(); + } + + $id = $authorization->getID(); + $title = pht('Authorization %d', $id); + + $blueprint = $authorization->getBlueprint(); + $blueprint_id = $blueprint->getID(); + + $header = id(new PHUIHeaderView()) + ->setHeader($title) + ->setUser($viewer) + ->setPolicyObject($authorization); + + + $state = $authorization->getBlueprintAuthorizationState(); + $icon = DrydockAuthorization::getBlueprintStateIcon($state); + $name = DrydockAuthorization::getBlueprintStateName($state); + + $header->setStatus($icon, null, $name); + + $actions = $this->buildActionListView($authorization); + $properties = $this->buildPropertyListView($authorization); + $properties->setActionList($actions); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb( + pht('Blueprints'), + $this->getApplicationURI('blueprint/')); + $crumbs->addTextCrumb( + $blueprint->getBlueprintName(), + $this->getApplicationURI("blueprint/{$blueprint_id}/")); + $crumbs->addTextCrumb($title); + + $object_box = id(new PHUIObjectBoxView()) + ->setHeader($header) + ->addPropertyList($properties); + + return $this->buildApplicationPage( + array( + $crumbs, + $object_box, + ), + array( + 'title' => $title, + )); + + } + + private function buildActionListView(DrydockAuthorization $authorization) { + $viewer = $this->getViewer(); + $id = $authorization->getID(); + + $view = id(new PhabricatorActionListView()) + ->setUser($viewer) + ->setObjectURI($this->getRequest()->getRequestURI()) + ->setObject($authorization); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $authorization, + PhabricatorPolicyCapability::CAN_EDIT); + + $authorize_uri = $this->getApplicationURI("authorization/{$id}/authorize/"); + $decline_uri = $this->getApplicationURI("authorization/{$id}/decline/"); + + $state_authorized = DrydockAuthorization::BLUEPRINTAUTH_AUTHORIZED; + $state_declined = DrydockAuthorization::BLUEPRINTAUTH_DECLINED; + + $state = $authorization->getBlueprintAuthorizationState(); + $can_authorize = $can_edit && ($state != $state_authorized); + $can_decline = $can_edit && ($state != $state_declined); + + $view->addAction( + id(new PhabricatorActionView()) + ->setHref($authorize_uri) + ->setName(pht('Approve Authorization')) + ->setIcon('fa-check') + ->setWorkflow(true) + ->setDisabled(!$can_authorize)); + + $view->addAction( + id(new PhabricatorActionView()) + ->setHref($decline_uri) + ->setName(pht('Decline Authorization')) + ->setIcon('fa-times') + ->setWorkflow(true) + ->setDisabled(!$can_decline)); + + return $view; + } + + private function buildPropertyListView(DrydockAuthorization $authorization) { + $viewer = $this->getViewer(); + + $object_phid = $authorization->getObjectPHID(); + $handles = $viewer->loadHandles(array($object_phid)); + $handle = $handles[$object_phid]; + + $view = new PHUIPropertyListView(); + + $view->addProperty( + pht('Authorized Object'), + $handle->renderLink($handle->getFullName())); + + $view->addProperty(pht('Object Type'), $handle->getTypeName()); + + $object_state = $authorization->getObjectAuthorizationState(); + + $view->addProperty( + pht('Authorization State'), + DrydockAuthorization::getObjectStateName($object_state)); + + return $view; + } + + public function buildSideNavView() { + // TODO: Get rid of this, but it's currently required by DrydockController. + return null; + } + + public function buildApplicationMenu() { + // TODO: As above. + return null; + } + +} diff --git a/src/applications/drydock/controller/DrydockBlueprintViewController.php b/src/applications/drydock/controller/DrydockBlueprintViewController.php index 7102962600..f90dcb9d82 100644 --- a/src/applications/drydock/controller/DrydockBlueprintViewController.php +++ b/src/applications/drydock/controller/DrydockBlueprintViewController.php @@ -51,6 +51,8 @@ final class DrydockBlueprintViewController extends DrydockBlueprintController { $resource_box = $this->buildResourceBox($blueprint); + $authorizations_box = $this->buildAuthorizationsBox($blueprint); + $timeline = $this->buildTransactionTimeline( $blueprint, new DrydockBlueprintTransactionQuery()); @@ -68,6 +70,7 @@ final class DrydockBlueprintViewController extends DrydockBlueprintController { $crumbs, $object_box, $resource_box, + $authorizations_box, $log_box, $timeline, ), @@ -167,12 +170,78 @@ final class DrydockBlueprintViewController extends DrydockBlueprintController { ->setTag('a') ->setHref($resources_uri) ->setIconFont('fa-search') - ->setText(pht('View All Resources'))); + ->setText(pht('View All'))); return id(new PHUIObjectBoxView()) ->setHeader($resource_header) ->setObjectList($resource_list); } + private function buildAuthorizationsBox(DrydockBlueprint $blueprint) { + $viewer = $this->getViewer(); + + $limit = 25; + + // If there are pending authorizations against this blueprint, make sure + // we show them first. + + $pending_authorizations = id(new DrydockAuthorizationQuery()) + ->setViewer($viewer) + ->withBlueprintPHIDs(array($blueprint->getPHID())) + ->withObjectStates( + array( + DrydockAuthorization::OBJECTAUTH_ACTIVE, + )) + ->withBlueprintStates( + array( + DrydockAuthorization::BLUEPRINTAUTH_REQUESTED, + )) + ->setLimit($limit) + ->execute(); + + $all_authorizations = id(new DrydockAuthorizationQuery()) + ->setViewer($viewer) + ->withBlueprintPHIDs(array($blueprint->getPHID())) + ->withObjectStates( + array( + DrydockAuthorization::OBJECTAUTH_ACTIVE, + )) + ->withBlueprintStates( + array( + DrydockAuthorization::BLUEPRINTAUTH_REQUESTED, + DrydockAuthorization::BLUEPRINTAUTH_AUTHORIZED, + )) + ->setLimit($limit) + ->execute(); + + $authorizations = + mpull($pending_authorizations, null, 'getPHID') + + mpull($all_authorizations, null, 'getPHID'); + + $authorization_list = id(new DrydockAuthorizationListView()) + ->setUser($viewer) + ->setAuthorizations($authorizations) + ->setNoDataString( + pht('No objects have active authorizations to use this blueprint.')); + + $id = $blueprint->getID(); + $authorizations_uri = "blueprint/{$id}/authorizations/query/all/"; + $authorizations_uri = $this->getApplicationURI($authorizations_uri); + + $authorizations_header = id(new PHUIHeaderView()) + ->setHeader(pht('Active Authorizations')) + ->addActionLink( + id(new PHUIButtonView()) + ->setTag('a') + ->setHref($authorizations_uri) + ->setIconFont('fa-search') + ->setText(pht('View All'))); + + return id(new PHUIObjectBoxView()) + ->setHeader($authorizations_header) + ->setObjectList($authorization_list); + + } + } diff --git a/src/applications/drydock/controller/DrydockController.php b/src/applications/drydock/controller/DrydockController.php index 760334cbdf..67e4278ae3 100644 --- a/src/applications/drydock/controller/DrydockController.php +++ b/src/applications/drydock/controller/DrydockController.php @@ -105,7 +105,7 @@ abstract class DrydockController extends PhabricatorController { ->setTag('a') ->setHref($all_uri) ->setIconFont('fa-search') - ->setText(pht('View All Logs'))); + ->setText(pht('View All'))); return id(new PHUIObjectBoxView()) ->setHeader($log_header) diff --git a/src/applications/drydock/controller/DrydockResourceViewController.php b/src/applications/drydock/controller/DrydockResourceViewController.php index 4809cf970c..71e4a09db1 100644 --- a/src/applications/drydock/controller/DrydockResourceViewController.php +++ b/src/applications/drydock/controller/DrydockResourceViewController.php @@ -170,7 +170,7 @@ final class DrydockResourceViewController extends DrydockResourceController { ->setTag('a') ->setHref($leases_uri) ->setIconFont('fa-search') - ->setText(pht('View All Leases'))); + ->setText(pht('View All'))); $lease_list = id(new DrydockLeaseListView()) ->setUser($viewer) diff --git a/src/applications/drydock/customfield/DrydockBlueprintCoreCustomField.php b/src/applications/drydock/customfield/DrydockBlueprintCoreCustomField.php index f4e6ba3a27..9af418b4d1 100644 --- a/src/applications/drydock/customfield/DrydockBlueprintCoreCustomField.php +++ b/src/applications/drydock/customfield/DrydockBlueprintCoreCustomField.php @@ -41,11 +41,6 @@ final class DrydockBlueprintCoreCustomField $object->setDetail($key, $value); } - public function applyApplicationTransactionExternalEffects( - PhabricatorApplicationTransaction $xaction) { - return; - } - public function getBlueprintFieldValue() { return $this->getProxy()->getFieldValue(); } diff --git a/src/applications/drydock/phid/DrydockAuthorizationPHIDType.php b/src/applications/drydock/phid/DrydockAuthorizationPHIDType.php new file mode 100644 index 0000000000..e518149945 --- /dev/null +++ b/src/applications/drydock/phid/DrydockAuthorizationPHIDType.php @@ -0,0 +1,37 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $authorization = $objects[$phid]; + $id = $authorization->getID(); + + $handle->setName(pht('Drydock Authorization %d', $id)); + $handle->setURI("/drydock/authorization/{$id}/"); + } + } + +} diff --git a/src/applications/drydock/phid/DrydockBlueprintPHIDType.php b/src/applications/drydock/phid/DrydockBlueprintPHIDType.php index 86eeb7f3c5..3d19198192 100644 --- a/src/applications/drydock/phid/DrydockBlueprintPHIDType.php +++ b/src/applications/drydock/phid/DrydockBlueprintPHIDType.php @@ -28,9 +28,12 @@ final class DrydockBlueprintPHIDType extends PhabricatorPHIDType { foreach ($handles as $phid => $handle) { $blueprint = $objects[$phid]; $id = $blueprint->getID(); + $name = $blueprint->getBlueprintName(); - $handle->setName($blueprint->getBlueprintName()); - $handle->setURI("/drydock/blueprint/{$id}/"); + $handle + ->setName($name) + ->setFullName(pht('Blueprint %d: %s', $id, $name)) + ->setURI("/drydock/blueprint/{$id}/"); } } diff --git a/src/applications/drydock/query/DrydockAuthorizationQuery.php b/src/applications/drydock/query/DrydockAuthorizationQuery.php new file mode 100644 index 0000000000..6d2cddcf8a --- /dev/null +++ b/src/applications/drydock/query/DrydockAuthorizationQuery.php @@ -0,0 +1,146 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withBlueprintPHIDs(array $phids) { + $this->blueprintPHIDs = $phids; + return $this; + } + + public function withObjectPHIDs(array $phids) { + $this->objectPHIDs = $phids; + return $this; + } + + public function withBlueprintStates(array $states) { + $this->blueprintStates = $states; + return $this; + } + + public function withObjectStates(array $states) { + $this->objectStates = $states; + return $this; + } + + public function newResultObject() { + return new DrydockAuthorization(); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function willFilterPage(array $authorizations) { + $blueprint_phids = mpull($authorizations, 'getBlueprintPHID'); + if ($blueprint_phids) { + $blueprints = id(new DrydockBlueprintQuery()) + ->setViewer($this->getViewer()) + ->setParentQuery($this) + ->withPHIDs($blueprint_phids) + ->execute(); + $blueprints = mpull($blueprints, null, 'getPHID'); + } else { + $blueprints = array(); + } + + foreach ($authorizations as $key => $authorization) { + $blueprint = idx($blueprints, $authorization->getBlueprintPHID()); + if (!$blueprint) { + $this->didRejectResult($authorization); + unset($authorizations[$key]); + continue; + } + $authorization->attachBlueprint($blueprint); + } + + $object_phids = mpull($authorizations, 'getObjectPHID'); + if ($object_phids) { + $objects = id(new PhabricatorObjectQuery()) + ->setViewer($this->getViewer()) + ->setParentQuery($this) + ->withPHIDs($object_phids) + ->execute(); + $objects = mpull($objects, null, 'getPHID'); + } else { + $objects = array(); + } + + foreach ($authorizations as $key => $authorization) { + $object = idx($objects, $authorization->getObjectPHID()); + if (!$object) { + $this->didRejectResult($authorization); + unset($authorizations[$key]); + continue; + } + $authorization->attachObject($object); + } + + return $authorizations; + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn, + 'phid IN (%Ls)', + $this->phids); + } + + if ($this->blueprintPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'blueprintPHID IN (%Ls)', + $this->blueprintPHIDs); + } + + if ($this->objectPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'objectPHID IN (%Ls)', + $this->objectPHIDs); + } + + if ($this->blueprintStates !== null) { + $where[] = qsprintf( + $conn, + 'blueprintAuthorizationState IN (%Ls)', + $this->blueprintStates); + } + + if ($this->objectStates !== null) { + $where[] = qsprintf( + $conn, + 'objectAuthorizationState IN (%Ls)', + $this->objectStates); + } + + return $where; + } + +} diff --git a/src/applications/drydock/query/DrydockAuthorizationSearchEngine.php b/src/applications/drydock/query/DrydockAuthorizationSearchEngine.php new file mode 100644 index 0000000000..7aaef65650 --- /dev/null +++ b/src/applications/drydock/query/DrydockAuthorizationSearchEngine.php @@ -0,0 +1,87 @@ +blueprint = $blueprint; + return $this; + } + + public function getBlueprint() { + return $this->blueprint; + } + + public function getResultTypeDescription() { + return pht('Drydock Authorizations'); + } + + public function getApplicationClassName() { + return 'PhabricatorDrydockApplication'; + } + + public function canUseInPanelContext() { + return false; + } + + public function newQuery() { + $query = new DrydockAuthorizationQuery(); + + $blueprint = $this->getBlueprint(); + $query->withBlueprintPHIDs(array($blueprint->getPHID())); + + return $query; + } + + protected function buildQueryFromParameters(array $map) { + $query = $this->newQuery(); + + return $query; + } + + protected function buildCustomSearchFields() { + return array(); + } + + protected function getURI($path) { + $blueprint = $this->getBlueprint(); + $id = $blueprint->getID(); + return "/drydock/blueprint/{$id}/authorizations/".$path; + } + + protected function getBuiltinQueryNames() { + return array( + 'all' => pht('All Authorizations'), + ); + } + + public function buildSavedQueryFromBuiltin($query_key) { + $query = $this->newSavedQuery(); + $query->setQueryKey($query_key); + + switch ($query_key) { + case 'all': + return $query; + } + + return parent::buildSavedQueryFromBuiltin($query_key); + } + + protected function renderResultList( + array $authorizations, + PhabricatorSavedQuery $query, + array $handles) { + + $list = id(new DrydockAuthorizationListView()) + ->setUser($this->requireViewer()) + ->setAuthorizations($authorizations); + + $result = new PhabricatorApplicationSearchResultView(); + $result->setTable($list); + + return $result; + } + +} diff --git a/src/applications/drydock/storage/DrydockAuthorization.php b/src/applications/drydock/storage/DrydockAuthorization.php new file mode 100644 index 0000000000..2bb5decb14 --- /dev/null +++ b/src/applications/drydock/storage/DrydockAuthorization.php @@ -0,0 +1,122 @@ + true, + self::CONFIG_COLUMN_SCHEMA => array( + 'blueprintAuthorizationState' => 'text32', + 'objectAuthorizationState' => 'text32', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_unique' => array( + 'columns' => array('objectPHID', 'blueprintPHID'), + 'unique' => true, + ), + 'key_blueprint' => array( + 'columns' => array('blueprintPHID', 'blueprintAuthorizationState'), + ), + 'key_object' => array( + 'columns' => array('objectPHID', 'objectAuthorizationState'), + ), + ), + ) + parent::getConfiguration(); + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID( + DrydockAuthorizationPHIDType::TYPECONST); + } + + public function attachBlueprint(DrydockBlueprint $blueprint) { + $this->blueprint = $blueprint; + return $this; + } + + public function getBlueprint() { + return $this->assertAttached($this->blueprint); + } + + public function attachObject($object) { + $this->object = $object; + return $this; + } + + public function getObject() { + return $this->assertAttached($this->object); + } + + public static function getBlueprintStateIcon($state) { + $map = array( + self::BLUEPRINTAUTH_REQUESTED => 'fa-exclamation-circle indigo', + self::BLUEPRINTAUTH_AUTHORIZED => 'fa-check-circle green', + self::BLUEPRINTAUTH_DECLINED => 'fa-times red', + ); + + return idx($map, $state, null); + } + + public static function getBlueprintStateName($state) { + $map = array( + self::BLUEPRINTAUTH_REQUESTED => pht('Requested'), + self::BLUEPRINTAUTH_AUTHORIZED => pht('Authorized'), + self::BLUEPRINTAUTH_DECLINED => pht('Declined'), + ); + + return idx($map, $state, pht('', $state)); + } + + public static function getObjectStateName($state) { + $map = array( + self::OBJECTAUTH_ACTIVE => pht('Active'), + self::OBJECTAUTH_INACTIVE => pht('Inactive'), + ); + + return idx($map, $state, pht('', $state)); + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } + + public function getPolicy($capability) { + return $this->getBlueprint()->getPolicy($capability); + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return $this->getBlueprint()->hasAutomaticCapability($capability, $viewer); + } + + public function describeAutomaticCapability($capability) { + return pht( + 'An authorization inherits the policies of the blueprint it '. + 'authorizes access to.'); + } + + +} diff --git a/src/applications/drydock/view/DrydockAuthorizationListView.php b/src/applications/drydock/view/DrydockAuthorizationListView.php new file mode 100644 index 0000000000..28296b6a3a --- /dev/null +++ b/src/applications/drydock/view/DrydockAuthorizationListView.php @@ -0,0 +1,65 @@ +authorizations = $authorizations; + return $this; + } + + public function setNoDataString($string) { + $this->noDataString = $string; + return $this; + } + + public function getNoDataString() { + return $this->noDataString; + } + + public function render() { + $viewer = $this->getUser(); + + $authorizations = $this->authorizations; + + $view = new PHUIObjectItemListView(); + + $nodata = $this->getNoDataString(); + if ($nodata) { + $view->setNoDataString($nodata); + } + + $handles = $viewer->loadHandles(mpull($authorizations, 'getObjectPHID')); + + foreach ($authorizations as $authorization) { + $id = $authorization->getID(); + $object_phid = $authorization->getObjectPHID(); + $handle = $handles[$object_phid]; + + $item = id(new PHUIObjectItemView()) + ->setHref("/drydock/authorization/{$id}/") + ->setObjectName(pht('Authorization %d', $id)) + ->setHeader($handle->getFullName()); + + $item->addAttribute($handle->getTypeName()); + + $object_state = $authorization->getObjectAuthorizationState(); + $item->addAttribute( + DrydockAuthorization::getObjectStateName($object_state)); + + $state = $authorization->getBlueprintAuthorizationState(); + $icon = DrydockAuthorization::getBlueprintStateIcon($state); + $name = DrydockAuthorization::getBlueprintStateName($state); + + $item->setStatusIcon($icon, $name); + + $view->addItem($item); + } + + return $view; + } + +} diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldBlueprints.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldBlueprints.php new file mode 100644 index 0000000000..29edd6d472 --- /dev/null +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldBlueprints.php @@ -0,0 +1,147 @@ +getObjectPHID(); + + $old = $this->decodeValue($xaction->getOldValue()); + $new = $this->decodeValue($xaction->getNewValue()); + + $old_phids = array_fuse($old); + $new_phids = array_fuse($new); + + $rem_phids = array_diff_key($old_phids, $new_phids); + $add_phids = array_diff_key($new_phids, $old_phids); + + $altered_phids = $rem_phids + $add_phids; + + if (!$altered_phids) { + return; + } + + $authorizations = id(new DrydockAuthorizationQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withObjectPHIDs(array($object_phid)) + ->withBlueprintPHIDs($altered_phids) + ->execute(); + $authorizations = mpull($authorizations, null, 'getBlueprintPHID'); + + $state_active = DrydockAuthorization::OBJECTAUTH_ACTIVE; + $state_inactive = DrydockAuthorization::OBJECTAUTH_INACTIVE; + + $state_requested = DrydockAuthorization::BLUEPRINTAUTH_REQUESTED; + + // Disable the object side of the authorization for any existing + // authorizations. + foreach ($rem_phids as $rem_phid) { + $authorization = idx($authorizations, $rem_phid); + if (!$authorization) { + continue; + } + + $authorization + ->setObjectAuthorizationState($state_inactive) + ->save(); + } + + // For new authorizations, either add them or reactivate them depending + // on the current state. + foreach ($add_phids as $add_phid) { + $needs_update = false; + + $authorization = idx($authorizations, $add_phid); + if (!$authorization) { + $authorization = id(new DrydockAuthorization()) + ->setObjectPHID($object_phid) + ->setObjectAuthorizationState($state_active) + ->setBlueprintPHID($add_phid) + ->setBlueprintAuthorizationState($state_requested); + + $needs_update = true; + } else { + $current_state = $authorization->getObjectAuthorizationState(); + if ($current_state != $state_active) { + $authorization->setObjectAuthorizationState($state_active); + $needs_update = true; + } + } + + if ($needs_update) { + $authorization->save(); + } + } + + } + + public function renderPropertyViewValue(array $handles) { + $value = $this->getFieldValue(); + if (!$value) { + return phutil_tag('em', array(), pht('No authorized blueprints.')); + } + + $object = $this->getObject(); + $object_phid = $object->getPHID(); + + // NOTE: We're intentionally letting you see the authorization state on + // blueprints you can't see because this has a tremendous potential to + // be extremely confusing otherwise. You still can't see the blueprints + // themselves, but you can know if the object is authorized on something. + + if ($value) { + $handles = $this->getViewer()->loadHandles($value); + + $authorizations = id(new DrydockAuthorizationQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withObjectPHIDs(array($object_phid)) + ->withBlueprintPHIDs($value) + ->execute(); + $authorizations = mpull($authorizations, null, 'getBlueprintPHID'); + } else { + $handles = array(); + $authorizations = array(); + } + + $items = array(); + foreach ($value as $phid) { + $authorization = idx($authorizations, $phid); + if (!$authorization) { + continue; + } + + $handle = $handles[$phid]; + + $item = id(new PHUIStatusItemView()) + ->setTarget($handle->renderLink()); + + $state = $authorization->getBlueprintAuthorizationState(); + $item->setIcon( + DrydockAuthorization::getBlueprintStateIcon($state), + null, + DrydockAuthorization::getBlueprintStateName($state)); + + $items[] = $item; + } + + $status = new PHUIStatusListView(); + foreach ($items as $item) { + $status->addItem($item); + } + + return $status; + } + + + +} diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php index ecaf67caa9..c7be0f7acb 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php @@ -217,7 +217,7 @@ abstract class PhabricatorStandardCustomFieldPHIDs return array(); } - private function decodeValue($value) { + protected function decodeValue($value) { $value = json_decode($value); if (!is_array($value)) { $value = array();