diff --git a/resources/sql/autopatches/20151009.drydock.auth.1.sql b/resources/sql/autopatches/20151009.drydock.auth.1.sql new file mode 100644 --- /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 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -799,6 +799,14 @@ '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 @@ '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 @@ '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 @@ '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 --- a/src/applications/drydock/application/PhabricatorDrydockApplication.php +++ b/src/applications/drydock/application/PhabricatorDrydockApplication.php @@ -57,6 +57,8 @@ 'DrydockResourceListController', 'logs/(?:query/(?P[^/]+)/)?' => 'DrydockLogListController', + 'authorizations/(?:query/(?P[^/]+)/)?' => + 'DrydockAuthorizationListController', ), 'create/' => 'DrydockBlueprintCreateController', 'edit/(?:(?P[1-9]\d*)/)?' => 'DrydockBlueprintEditController', @@ -81,6 +83,13 @@ '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 --- a/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php +++ b/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php @@ -390,5 +390,15 @@ 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 --- /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 --- /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 --- /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 --- a/src/applications/drydock/controller/DrydockBlueprintViewController.php +++ b/src/applications/drydock/controller/DrydockBlueprintViewController.php @@ -51,6 +51,8 @@ $resource_box = $this->buildResourceBox($blueprint); + $authorizations_box = $this->buildAuthorizationsBox($blueprint); + $timeline = $this->buildTransactionTimeline( $blueprint, new DrydockBlueprintTransactionQuery()); @@ -68,6 +70,7 @@ $crumbs, $object_box, $resource_box, + $authorizations_box, $log_box, $timeline, ), @@ -167,12 +170,78 @@ ->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 --- a/src/applications/drydock/controller/DrydockController.php +++ b/src/applications/drydock/controller/DrydockController.php @@ -105,7 +105,7 @@ ->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 --- a/src/applications/drydock/controller/DrydockResourceViewController.php +++ b/src/applications/drydock/controller/DrydockResourceViewController.php @@ -170,7 +170,7 @@ ->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 --- a/src/applications/drydock/customfield/DrydockBlueprintCoreCustomField.php +++ b/src/applications/drydock/customfield/DrydockBlueprintCoreCustomField.php @@ -41,11 +41,6 @@ $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 --- /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 --- a/src/applications/drydock/phid/DrydockBlueprintPHIDType.php +++ b/src/applications/drydock/phid/DrydockBlueprintPHIDType.php @@ -28,9 +28,12 @@ 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 --- /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 --- /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 --- /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 --- /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 --- /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 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php @@ -217,7 +217,7 @@ return array(); } - private function decodeValue($value) { + protected function decodeValue($value) { $value = json_decode($value); if (!is_array($value)) { $value = array();