1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-29 00:40:57 +01:00

Add basic per-object privacy policies

Summary:
Provides a basic start for access policies. Objects expose various capabilities, like CAN_VIEW, CAN_EDIT, etc., and set a policy for each capability. We currently implement three policies, PUBLIC (anyone, including logged-out), USERS (any logged-in) and NOONE (nobody). There's also a way to provide automatic capability grants (e.g., the owner of an object can always see it, even if some capability is set to "NOONE"), but I'm not sure how great the implementation feels and it might change.

Most of the code here is providing a primitive for efficient policy-aware list queries. The problem with doing queries naively is that you have to do crazy amounts of filtering, e.g. to show the user page 6, you need to filter at least 600 objects (and likely more) before you can figure out which ones are 500-600 for them. You can't just do "LIMIT 500, 100" because that might have only 50 results, or no results. Instead, the query looks like "WHERE id > last_visible_id", and then we fetch additional pages as necessary to satisfy the request.

The general idea is that we move all data access to Query classes and have them do object filtering. The ID paging primitive allows efficient paging in most cases, and the executeOne() method provides a concise way to do policy checks for edit/view screens.

We'll probably end up with mostly broader policy UIs or configuration-based policies, but there are at least a few cases for per-object privacy (e.g., marking tasks as "Security", and restricting things to the members of projects) so I figured we'd start with a flexible primitive and the simplify it in the UI where we can.

Test Plan: Unit tests, played around in the UI with various policy settings.

Reviewers: btrahan, vrana, jungejason

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T603

Differential Revision: https://secure.phabricator.com/D2210
This commit is contained in:
epriestley 2012-04-14 10:13:29 -07:00
parent 6be9f6f3a8
commit ded641ae32
36 changed files with 1297 additions and 134 deletions

View file

@ -39,6 +39,7 @@ phutil_register_library_map(array(
'AphrontFormLayoutView' => 'view/form/layout',
'AphrontFormMarkupControl' => 'view/form/control/markup',
'AphrontFormPasswordControl' => 'view/form/control/password',
'AphrontFormPolicyControl' => 'view/form/control/policy',
'AphrontFormRadioButtonControl' => 'view/form/control/radio',
'AphrontFormRecaptchaControl' => 'view/form/control/recaptcha',
'AphrontFormSelectControl' => 'view/form/control/select',
@ -54,6 +55,7 @@ phutil_register_library_map(array(
'AphrontHeadsupActionListView' => 'view/layout/headsup/actionlist',
'AphrontHeadsupActionView' => 'view/layout/headsup/action',
'AphrontHeadsupView' => 'view/layout/headsup/panel',
'AphrontIDPagerView' => 'view/control/idpager',
'AphrontIsolatedDatabaseConnection' => 'storage/connection/isolated',
'AphrontIsolatedDatabaseConnectionTestCase' => 'storage/connection/isolated/__tests__',
'AphrontIsolatedHTTPSink' => 'aphront/sink/test',
@ -633,6 +635,7 @@ phutil_register_library_map(array(
'PhabricatorHash' => 'infrastructure/util/hash',
'PhabricatorHelpController' => 'applications/help/controller/base',
'PhabricatorHelpKeyboardShortcutController' => 'applications/help/controller/keyboardshortcut',
'PhabricatorIDPagedPolicyQuery' => 'infrastructure/query/policy/idpaged',
'PhabricatorIRCBot' => 'infrastructure/daemon/irc/bot',
'PhabricatorIRCDifferentialNotificationHandler' => 'infrastructure/daemon/irc/handler/differentialnotification',
'PhabricatorIRCHandler' => 'infrastructure/daemon/irc/handler/base',
@ -745,12 +748,23 @@ phutil_register_library_map(array(
'PhabricatorPasteController' => 'applications/paste/controller/base',
'PhabricatorPasteDAO' => 'applications/paste/storage/base',
'PhabricatorPasteListController' => 'applications/paste/controller/list',
'PhabricatorPasteQuery' => 'applications/paste/query/paste',
'PhabricatorPasteViewController' => 'applications/paste/controller/view',
'PhabricatorPeopleController' => 'applications/people/controller/base',
'PhabricatorPeopleEditController' => 'applications/people/controller/edit',
'PhabricatorPeopleListController' => 'applications/people/controller/list',
'PhabricatorPeopleLogsController' => 'applications/people/controller/logs',
'PhabricatorPeopleProfileController' => 'applications/people/controller/profile',
'PhabricatorPolicies' => 'applications/policy/constants/policy',
'PhabricatorPolicyCapability' => 'applications/policy/constants/capability',
'PhabricatorPolicyConstants' => 'applications/policy/constants/base',
'PhabricatorPolicyException' => 'applications/policy/exception/base',
'PhabricatorPolicyFilter' => 'applications/policy/filter/policy',
'PhabricatorPolicyInterface' => 'applications/policy/interface/policy',
'PhabricatorPolicyQuery' => 'infrastructure/query/policy/base',
'PhabricatorPolicyTestCase' => 'applications/policy/__tests__',
'PhabricatorPolicyTestObject' => 'applications/policy/__tests__',
'PhabricatorPolicyTestQuery' => 'applications/policy/__tests__',
'PhabricatorProfileHeaderView' => 'view/layout/profileheader',
'PhabricatorProject' => 'applications/project/storage/project',
'PhabricatorProjectAffiliation' => 'applications/project/storage/affiliation',
@ -1023,6 +1037,7 @@ phutil_register_library_map(array(
'AphrontFormLayoutView' => 'AphrontView',
'AphrontFormMarkupControl' => 'AphrontFormControl',
'AphrontFormPasswordControl' => 'AphrontFormControl',
'AphrontFormPolicyControl' => 'AphrontFormControl',
'AphrontFormRadioButtonControl' => 'AphrontFormControl',
'AphrontFormRecaptchaControl' => 'AphrontFormControl',
'AphrontFormSelectControl' => 'AphrontFormControl',
@ -1037,6 +1052,7 @@ phutil_register_library_map(array(
'AphrontHeadsupActionListView' => 'AphrontView',
'AphrontHeadsupActionView' => 'AphrontView',
'AphrontHeadsupView' => 'AphrontView',
'AphrontIDPagerView' => 'AphrontView',
'AphrontIsolatedDatabaseConnection' => 'AphrontDatabaseConnection',
'AphrontIsolatedDatabaseConnectionTestCase' => 'PhabricatorTestCase',
'AphrontIsolatedHTTPSink' => 'AphrontHTTPSink',
@ -1502,6 +1518,7 @@ phutil_register_library_map(array(
'PhabricatorGoodForNothingWorker' => 'PhabricatorWorker',
'PhabricatorHelpController' => 'PhabricatorController',
'PhabricatorHelpKeyboardShortcutController' => 'PhabricatorHelpController',
'PhabricatorIDPagedPolicyQuery' => 'PhabricatorPolicyQuery',
'PhabricatorIRCBot' => 'PhabricatorDaemon',
'PhabricatorIRCDifferentialNotificationHandler' => 'PhabricatorIRCHandler',
'PhabricatorIRCLogHandler' => 'PhabricatorIRCHandler',
@ -1594,12 +1611,18 @@ phutil_register_library_map(array(
'PhabricatorPasteController' => 'PhabricatorController',
'PhabricatorPasteDAO' => 'PhabricatorLiskDAO',
'PhabricatorPasteListController' => 'PhabricatorPasteController',
'PhabricatorPasteQuery' => 'PhabricatorIDPagedPolicyQuery',
'PhabricatorPasteViewController' => 'PhabricatorPasteController',
'PhabricatorPeopleController' => 'PhabricatorController',
'PhabricatorPeopleEditController' => 'PhabricatorPeopleController',
'PhabricatorPeopleListController' => 'PhabricatorPeopleController',
'PhabricatorPeopleLogsController' => 'PhabricatorPeopleController',
'PhabricatorPeopleProfileController' => 'PhabricatorPeopleController',
'PhabricatorPolicies' => 'PhabricatorPolicyConstants',
'PhabricatorPolicyCapability' => 'PhabricatorPolicyConstants',
'PhabricatorPolicyQuery' => 'PhabricatorQuery',
'PhabricatorPolicyTestCase' => 'PhabricatorTestCase',
'PhabricatorPolicyTestQuery' => 'PhabricatorPolicyQuery',
'PhabricatorProfileHeaderView' => 'AphrontView',
'PhabricatorProject' => 'PhabricatorProjectDAO',
'PhabricatorProjectAffiliation' => 'PhabricatorProjectDAO',
@ -1804,5 +1827,13 @@ phutil_register_library_map(array(
array(
0 => 'PhabricatorInlineCommentInterface',
),
'PhabricatorPaste' =>
array(
0 => 'PhabricatorPolicyInterface',
),
'PhabricatorPolicyTestObject' =>
array(
0 => 'PhabricatorPolicyInterface',
),
),
));

View file

@ -468,11 +468,7 @@ class AphrontDefaultApplicationConfiguration
public function handleException(Exception $ex) {
// Always log the unhandled exception.
phlog($ex);
$class = phutil_escape_html(get_class($ex));
$message = phutil_escape_html($ex->getMessage());
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
$user = $this->getRequest()->getUser();
if (!$user) {
@ -480,6 +476,39 @@ class AphrontDefaultApplicationConfiguration
$user = new PhabricatorUser();
}
if ($ex instanceof PhabricatorPolicyException) {
$content =
'<div class="aphront-policy-exception">'.
phutil_escape_html($ex->getMessage()).
'</div>';
$dialog = new AphrontDialogView();
$dialog
->setTitle(
$is_serious
? 'Access Denied'
: "You Shall Not Pass")
->setClass('aphront-access-dialog')
->setUser($user)
->appendChild($content);
if ($this->getRequest()->isAjax()) {
$dialog->addCancelButton('/', 'Close');
} else {
$dialog->addCancelButton('/', $is_serious ? 'OK' : 'Away With Thee');
}
$response = new AphrontDialogResponse();
$response->setDialog($dialog);
return $response;
}
// Always log the unhandled exception.
phlog($ex);
$class = phutil_escape_html(get_class($ex));
$message = phutil_escape_html($ex->getMessage());
if (PhabricatorEnv::getEnvConfig('phabricator.show-stack-traces')) {
$trace = $this->renderStackTrace($ex->getTrace(), $user);
} else {

View file

@ -24,10 +24,6 @@ final class PhabricatorPasteListController extends PhabricatorPasteController {
private $paste;
private $pasteText;
private $offset;
private $pageSize;
private $author;
private function setFilter($filter) {
$this->filter = $filter;
return $this;
@ -40,6 +36,7 @@ final class PhabricatorPasteListController extends PhabricatorPasteController {
$this->errorView = $error_view;
return $this;
}
private function getErrorView() {
return $this->errorView;
}
@ -68,40 +65,19 @@ final class PhabricatorPasteListController extends PhabricatorPasteController {
return $this->pasteText;
}
private function setOffset($offset) {
$this->offset = $offset;
return $this;
}
private function getOffset() {
return $this->offset;
}
private function setPageSize($page_size) {
$this->pageSize = $page_size;
return $this;
}
private function getPageSize() {
return $this->pageSize;
}
private function setAuthor($author) {
$this->author = $author;
return $this;
}
private function getAuthor() {
return $this->author;
}
public function willProcessRequest(array $data) {
$this->setFilter(idx($data, 'filter', 'create'));
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$paste_list = array();
$pager = null;
$pager = new AphrontIDPagerView();
$pager->readFromRequest($request);
$query = new PhabricatorPasteQuery();
$query->setViewer($user);
switch ($this->getFilter()) {
case 'create':
@ -111,22 +87,18 @@ final class PhabricatorPasteListController extends PhabricatorPasteController {
if ($created_paste_redirect) {
return $created_paste_redirect;
}
// if we didn't succeed or we weren't trying, load just a few
// recent pastes with NO pagination
$this->setOffset(0);
$this->setPageSize(10);
list($paste_list, $pager) = $this->loadPasteList();
break;
$query->setLimit(10);
$paste_list = $query->execute();
$pager = null;
break;
case 'my':
$this->setAuthor($user->getPHID());
$this->setOffset($request->getInt('page', 0));
list($paste_list, $pager) = $this->loadPasteList();
$query->withAuthorPHIDs(array($user->getPHID()));
$paste_list = $query->executeWithPager($pager);
break;
case 'all':
$this->setOffset($request->getInt('page', 0));
list($paste_list, $pager) = $this->loadPasteList();
$paste_list = $query->executeWithPager($pager);
break;
}
@ -171,24 +143,19 @@ final class PhabricatorPasteListController extends PhabricatorPasteController {
),
'See all Pastes');
$header = "Recent Pastes &middot; {$see_all}";
$side_nav->appendChild($this->renderPasteList($paste_list,
$header,
$pager = null));
break;
case 'my':
$header = 'Your Pastes';
$side_nav->appendChild($this->renderPasteList($paste_list,
$header,
$pager));
break;
case 'all':
$header = 'All Pastes';
$side_nav->appendChild($this->renderPasteList($paste_list,
$header,
$pager));
break;
}
$side_nav->appendChild(
$this->renderPasteList($paste_list, $header, $pager));
return $this->buildStandardPageResponse(
$side_nav,
array(
@ -282,30 +249,66 @@ final class PhabricatorPasteListController extends PhabricatorPasteController {
$this->setPaste($new_paste);
}
private function loadPasteList() {
private function renderCreatePaste() {
$request = $this->getRequest();
$user = $request->getUser();
$pager = new AphrontPagerView();
$pager->setOffset($this->getOffset());
if ($this->getPageSize()) {
$pager->setPageSize($this->getPageSize());
}
$new_paste = $this->getPaste();
if ($this->getAuthor()) {
$pastes = id(new PhabricatorPaste())->loadAllWhere(
'authorPHID = %s ORDER BY id DESC LIMIT %d, %d',
$this->getAuthor(),
$pager->getOffset(),
$pager->getPageSize() + 1);
} else {
$pastes = id(new PhabricatorPaste())->loadAllWhere(
'1 = 1 ORDER BY id DESC LIMIT %d, %d',
$pager->getOffset(),
$pager->getPageSize() + 1);
}
$form = new AphrontFormView();
$pastes = $pager->sliceResults($pastes);
$pager->setURI($request->getRequestURI(), 'page');
$available_languages = PhabricatorEnv::getEnvConfig(
'pygments.dropdown-choices');
asort($available_languages);
$language_select = id(new AphrontFormSelectControl())
->setLabel('Language')
->setName('language')
->setValue($new_paste->getLanguage())
->setOptions($available_languages);
$form
->setUser($user)
->setAction($request->getRequestURI()->getPath())
->addHiddenInput('parent', $new_paste->getParentPHID())
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Title')
->setValue($new_paste->getTitle())
->setName('title'))
->appendChild($language_select)
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel('Text')
->setError($this->getErrorText())
->setValue($this->getPasteText())
->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL)
->setName('text'))
/* TODO: Doesn't have any useful options yet.
->appendChild(
id(new AphrontFormPolicyControl())
->setLabel('Visible To')
->setUser($user)
->setValue(
$new_paste->getPolicy(PhabricatorPolicyCapability::CAN_VIEW))
->setName('policy'))
*/
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton('/paste/')
->setValue('Create Paste'));
$create_panel = new AphrontPanelView();
$create_panel->setWidth(AphrontPanelView::WIDTH_FULL);
$create_panel->setHeader('Create a Paste');
$create_panel->appendChild($form);
return $create_panel;
}
private function renderPasteList(array $pastes, $header, $pager) {
assert_instances_of($pastes, 'PhabricatorPaste');
$phids = mpull($pastes, 'getAuthorPHID');
$handles = array();
@ -318,8 +321,7 @@ final class PhabricatorPasteListController extends PhabricatorPasteController {
if ($phids) {
$files = id(new PhabricatorFile())->loadAllWhere(
'phid in (%Ls)',
$phids
);
$phids);
if ($files) {
$file_uris = mpull($files, 'getBestURI', 'getPHID');
}
@ -363,59 +365,7 @@ final class PhabricatorPasteListController extends PhabricatorPasteController {
);
}
return array($paste_list_rows, $pager);
}
private function renderCreatePaste() {
$request = $this->getRequest();
$user = $request->getUser();
$new_paste = $this->getPaste();
$form = new AphrontFormView();
$available_languages = PhabricatorEnv::getEnvConfig(
'pygments.dropdown-choices');
asort($available_languages);
$language_select = id(new AphrontFormSelectControl())
->setLabel('Language')
->setName('language')
->setValue($new_paste->getLanguage())
->setOptions($available_languages);
$form
->setUser($user)
->setAction($request->getRequestURI()->getPath())
->addHiddenInput('parent', $new_paste->getParentPHID())
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Title')
->setValue($new_paste->getTitle())
->setName('title'))
->appendChild($language_select)
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel('Text')
->setError($this->getErrorText())
->setValue($this->getPasteText())
->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL)
->setName('text'))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton('/paste/')
->setValue('Create Paste'));
$create_panel = new AphrontPanelView();
$create_panel->setWidth(AphrontPanelView::WIDTH_FULL);
$create_panel->setHeader('Create a Paste');
$create_panel->appendChild($form);
return $create_panel;
}
private function renderPasteList($paste_list_rows,
$header,
$pager = null) {
$table = new AphrontTableView($paste_list_rows);
$table->setHeaders(
array(

View file

@ -9,10 +9,11 @@
phutil_require_module('phabricator', 'aphront/response/redirect');
phutil_require_module('phabricator', 'applications/files/storage/file');
phutil_require_module('phabricator', 'applications/paste/controller/base');
phutil_require_module('phabricator', 'applications/paste/query/paste');
phutil_require_module('phabricator', 'applications/paste/storage/paste');
phutil_require_module('phabricator', 'applications/phid/handle/data');
phutil_require_module('phabricator', 'infrastructure/env');
phutil_require_module('phabricator', 'view/control/pager');
phutil_require_module('phabricator', 'view/control/idpager');
phutil_require_module('phabricator', 'view/control/table');
phutil_require_module('phabricator', 'view/form/base');
phutil_require_module('phabricator', 'view/form/control/select');

View file

@ -29,7 +29,11 @@ final class PhabricatorPasteViewController extends PhabricatorPasteController {
$request = $this->getRequest();
$user = $request->getUser();
$paste = id(new PhabricatorPaste())->load($this->id);
$paste = id(new PhabricatorPasteQuery())
->setViewer($user)
->withPasteIDs(array($this->id))
->executeOne();
if (!$paste) {
return new Aphront404Response();
}

View file

@ -11,6 +11,7 @@ phutil_require_module('phabricator', 'aphront/response/404');
phutil_require_module('phabricator', 'applications/files/storage/file');
phutil_require_module('phabricator', 'applications/markup/syntax');
phutil_require_module('phabricator', 'applications/paste/controller/base');
phutil_require_module('phabricator', 'applications/paste/query/paste');
phutil_require_module('phabricator', 'applications/paste/storage/paste');
phutil_require_module('phabricator', 'infrastructure/celerity/api');
phutil_require_module('phabricator', 'infrastructure/javelin/api');

View file

@ -0,0 +1,73 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class PhabricatorPasteQuery extends PhabricatorIDPagedPolicyQuery {
private $pasteIDs;
private $authorPHIDs;
public function withPasteIDs(array $ids) {
$this->pasteIDs = $ids;
return $this;
}
public function withAuthorPHIDs(array $phids) {
$this->authorPHIDs = $phids;
return $this;
}
public function loadPage() {
$table = new PhabricatorPaste();
$conn_r = $table->establishConnection('r');
$data = queryfx_all(
$conn_r,
'SELECT paste.* FROM %T paste %Q %Q %Q',
$table->getTableName(),
$this->buildWhereClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
$results = $table->loadAllFromArray($data);
return $this->processResults($results);
}
protected function buildWhereClause($conn_r) {
$where = array();
$where[] = $this->buildPagingClause($conn_r);
if ($this->pasteIDs) {
$where[] = qsprintf(
$conn_r,
'id IN (%Ls)',
$this->pasteIDs);
}
if ($this->authorPHIDs) {
$where[] = qsprintf(
$conn_r,
'authorPHID IN (%Ls)',
$this->authorPHIDs);
}
return $this->formatWhereClause($where);
}
}

View file

@ -0,0 +1,15 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'applications/paste/storage/paste');
phutil_require_module('phabricator', 'infrastructure/query/policy/idpaged');
phutil_require_module('phabricator', 'storage/qsprintf');
phutil_require_module('phabricator', 'storage/queryfx');
phutil_require_source('PhabricatorPasteQuery.php');

View file

@ -16,7 +16,8 @@
* limitations under the License.
*/
final class PhabricatorPaste extends PhabricatorPasteDAO {
final class PhabricatorPaste extends PhabricatorPasteDAO
implements PhabricatorPolicyInterface {
protected $phid;
protected $title;
@ -36,4 +37,18 @@ final class PhabricatorPaste extends PhabricatorPasteDAO {
PhabricatorPHIDConstants::PHID_TYPE_PSTE);
}
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
return PhabricatorPolicies::POLICY_USER;
}
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
return ($user->getPHID() == $this->getAuthorPHID());
}
}

View file

@ -9,6 +9,9 @@
phutil_require_module('phabricator', 'applications/paste/storage/base');
phutil_require_module('phabricator', 'applications/phid/constants');
phutil_require_module('phabricator', 'applications/phid/storage/phid');
phutil_require_module('phabricator', 'applications/policy/constants/capability');
phutil_require_module('phabricator', 'applications/policy/constants/policy');
phutil_require_module('phabricator', 'applications/policy/interface/policy');
phutil_require_source('PhabricatorPaste.php');

View file

@ -0,0 +1,138 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class PhabricatorPolicyTestCase extends PhabricatorTestCase {
/**
* Verify that any user can view an object with POLICY_PUBLIC.
*/
public function testPublicPolicy() {
$viewer = new PhabricatorUser();
$object = new PhabricatorPolicyTestObject();
$object->setCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
));
$object->setPolicies(
array(
PhabricatorPolicyCapability::CAN_VIEW =>
PhabricatorPolicies::POLICY_PUBLIC,
));
$query = new PhabricatorPolicyTestQuery();
$query->setResults(array($object));
$query->setViewer($viewer);
$result = $query->executeOne();
$this->assertEqual($object, $result, 'Policy: Public');
}
/**
* Verify that any logged-in user can view an object with POLICY_USER, but
* logged-out users can not.
*/
public function testUsersPolicy() {
$viewer = new PhabricatorUser();
$object = new PhabricatorPolicyTestObject();
$object->setCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
));
$object->setPolicies(
array(
PhabricatorPolicyCapability::CAN_VIEW =>
PhabricatorPolicies::POLICY_USER,
));
$query = new PhabricatorPolicyTestQuery();
$query->setResults(array($object));
$query->setViewer($viewer);
$caught = null;
try {
$query->executeOne();
} catch (PhabricatorPolicyException $ex) {
$caught = $ex;
}
$this->assertEqual(
true,
($caught instanceof PhabricatorPolicyException),
'Policy: Users rejects logged out users.');
$viewer->setPHID(1);
$result = $query->executeOne();
$this->assertEqual(
$object,
$result,
'Policy: Users');
}
/**
* Verify that no one can view an object with POLICY_NOONE.
*/
public function testNoOnePolicy() {
$viewer = new PhabricatorUser();
$object = new PhabricatorPolicyTestObject();
$object->setCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
));
$object->setPolicies(
array(
PhabricatorPolicyCapability::CAN_VIEW =>
PhabricatorPolicies::POLICY_NOONE,
));
$query = new PhabricatorPolicyTestQuery();
$query->setResults(array($object));
$query->setViewer($viewer);
$caught = null;
try {
$query->executeOne();
} catch (PhabricatorPolicyException $ex) {
$caught = $ex;
}
$this->assertEqual(
true,
($caught instanceof PhabricatorPolicyException),
'Policy: No One rejects logged out users.');
$viewer->setPHID(1);
$caught = null;
try {
$query->executeOne();
} catch (PhabricatorPolicyException $ex) {
$caught = $ex;
}
$this->assertEqual(
true,
($caught instanceof PhabricatorPolicyException),
'Policy: No One rejects logged-in users.');
}
}

View file

@ -0,0 +1,57 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Configurable test object for implementing Policy unit tests.
*/
final class PhabricatorPolicyTestObject
implements PhabricatorPolicyInterface {
private $capabilities = array();
private $policies = array();
private $automaticCapabilities = array();
public function getCapabilities() {
return $this->capabilities;
}
public function getPolicy($capability) {
return idx($this->policies, $capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
$auto = idx($this->automaticCapabilities, $capability, array());
return idx($auto, $viewer->getPHID());
}
public function setCapabilities(array $capabilities) {
$this->capabilities = $capabilities;
return $this;
}
public function setPolicies(array $policy_map) {
$this->policies = $policy_map;
return $this;
}
public function setAutomaticCapabilities(array $auto_map) {
$this->automaticCapabilities = $auto_map;
return $this;
}
}

View file

@ -0,0 +1,40 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Configurable test query for implementing Policy unit tests.
*/
final class PhabricatorPolicyTestQuery
extends PhabricatorPolicyQuery {
private $results;
public function setResults(array $results) {
$this->results = $results;
return $this;
}
public function loadPage() {
return $this->results;
}
public function nextPage(array $page) {
return null;
}
}

View file

@ -0,0 +1,21 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'applications/people/storage/user');
phutil_require_module('phabricator', 'applications/policy/constants/capability');
phutil_require_module('phabricator', 'applications/policy/constants/policy');
phutil_require_module('phabricator', 'applications/policy/interface/policy');
phutil_require_module('phabricator', 'infrastructure/query/policy/base');
phutil_require_module('phabricator', 'infrastructure/testing/testcase');
phutil_require_module('phutil', 'utils');
phutil_require_source('PhabricatorPolicyTestCase.php');
phutil_require_source('PhabricatorPolicyTestObject.php');
phutil_require_source('PhabricatorPolicyTestQuery.php');

View file

@ -0,0 +1,21 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
abstract class PhabricatorPolicyConstants {
}

View file

@ -0,0 +1,10 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_source('PhabricatorPolicyConstants.php');

View file

@ -0,0 +1,23 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class PhabricatorPolicyCapability extends PhabricatorPolicyConstants {
const CAN_VIEW = 'view';
}

View file

@ -0,0 +1,12 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'applications/policy/constants/base');
phutil_require_source('PhabricatorPolicyCapability.php');

View file

@ -0,0 +1,25 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class PhabricatorPolicies extends PhabricatorPolicyConstants {
const POLICY_PUBLIC = 'public';
const POLICY_USER = 'users';
const POLICY_NOONE = 'no-one';
}

View file

@ -0,0 +1,12 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'applications/policy/constants/base');
phutil_require_source('PhabricatorPolicies.php');

View file

@ -0,0 +1,21 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class PhabricatorPolicyException extends Exception {
}

View file

@ -0,0 +1,10 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_source('PhabricatorPolicyException.php');

View file

@ -0,0 +1,116 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class PhabricatorPolicyFilter {
private $viewer;
private $objects;
private $capability;
private $raisePolicyExceptions;
public function setViewer(PhabricatorUser $user) {
$this->viewer = $user;
return $this;
}
public function setCapability($capability) {
$this->capability = $capability;
return $this;
}
public function raisePolicyExceptions($raise) {
$this->raisePolicyExceptions = $raise;
return $this;
}
public function apply(array $objects) {
assert_instances_of($objects, 'PhabricatorPolicyInterface');
$viewer = $this->viewer;
$capability = $this->capability;
if (!$viewer || !$capability) {
throw new Exception(
'Call setViewer() and setCapability() before apply()!');
}
$filtered = array();
foreach ($objects as $key => $object) {
$object_capabilities = $object->getCapabilities();
if (!in_array($capability, $object_capabilities)) {
throw new Exception(
"Testing for capability '{$capability}' on an object which does not ".
"have that capability!");
}
if ($object->hasAutomaticCapability($capability, $this->viewer)) {
$filtered[$key] = $object;
continue;
}
$policy = $object->getPolicy($capability);
switch ($policy) {
case PhabricatorPolicies::POLICY_PUBLIC:
$filtered[$key] = $object;
break;
case PhabricatorPolicies::POLICY_USER:
if ($viewer->getPHID()) {
$filtered[$key] = $object;
} else {
$this->rejectObject($object, $policy);
}
break;
case PhabricatorPolicies::POLICY_NOONE:
$this->rejectObject($object, $policy);
break;
default:
throw new Exception("Object has unknown policy '{$policy}'!");
}
}
return $filtered;
}
private function rejectObject($object, $policy) {
if (!$this->raisePolicyExceptions) {
return;
}
$message = "You do not have permission to view this object.";
switch ($policy) {
case PhabricatorPolicies::POLICY_PUBLIC:
$who = "This is curious, since anyone can view the object.";
break;
case PhabricatorPolicies::POLICY_USER:
$who = "To view this object, you must be logged in.";
break;
case PhabricatorPolicies::POLICY_NOONE:
$who = "No one can view this object.";
break;
default:
$who = "It is unclear who can view this object.";
break;
}
throw new PhabricatorPolicyException("{$message} {$who}");
}
}

View file

@ -0,0 +1,15 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'applications/policy/constants/policy');
phutil_require_module('phabricator', 'applications/policy/exception/base');
phutil_require_module('phutil', 'utils');
phutil_require_source('PhabricatorPolicyFilter.php');

View file

@ -0,0 +1,25 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
interface PhabricatorPolicyInterface {
public function getCapabilities();
public function getPolicy($capability);
public function hasAutomaticCapability($capability, PhabricatorUser $viewer);
}

View file

@ -0,0 +1,10 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_source('PhabricatorPolicyInterface.php');

View file

@ -21,9 +21,12 @@ abstract class PhabricatorQuery {
abstract public function execute();
final protected function formatWhereClause(array $parts) {
$parts = array_filter($parts);
if (!$parts) {
return '';
}
return 'WHERE ('.implode(') AND (', $parts).')';
}

View file

@ -0,0 +1,96 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
abstract class PhabricatorPolicyQuery extends PhabricatorQuery {
private $limit;
private $viewer;
private $raisePolicyExceptions;
final public function setViewer($viewer) {
$this->viewer = $viewer;
return $this;
}
final public function getViewer() {
return $this->viewer;
}
final public function setLimit($limit) {
$this->limit = $limit;
return $this;
}
final public function getLimit() {
return $this->limit;
}
final public function executeOne() {
$this->raisePolicyExceptions = true;
try {
$results = $this->execute();
} catch (Exception $ex) {
$this->raisePolicyExceptions = false;
throw $ex;
}
if (count($results) > 1) {
throw new Exception("Expected a single result!");
}
return head($results);
}
final public function execute() {
if (!$this->viewer) {
throw new Exception("Call setViewer() before execute()!");
}
$results = array();
$filter = new PhabricatorPolicyFilter();
$filter->setViewer($this->viewer);
$filter->setCapability(PhabricatorPolicyCapability::CAN_VIEW);
$filter->raisePolicyExceptions($this->raisePolicyExceptions);
do {
$page = $this->loadPage();
$visible = $filter->apply($page);
foreach ($visible as $key => $result) {
$results[$key] = $result;
if ($this->getLimit() && count($results) >= $this->getLimit()) {
break 2;
}
}
if (!$this->getLimit() || (count($page) < $this->getLimit())) {
break;
}
$this->nextPage($page);
} while (true);
return $results;
}
abstract protected function loadPage();
abstract protected function nextPage(array $page);
}

View file

@ -0,0 +1,16 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'applications/policy/constants/capability');
phutil_require_module('phabricator', 'applications/policy/filter/policy');
phutil_require_module('phabricator', 'infrastructure/query/base');
phutil_require_module('phutil', 'utils');
phutil_require_source('PhabricatorPolicyQuery.php');

View file

@ -0,0 +1,129 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* A query class which uses ID-based paging. This paging is much more performant
* than offset-based paging in the presence of policy filtering.
*/
abstract class PhabricatorIDPagedPolicyQuery extends PhabricatorPolicyQuery {
private $afterID;
private $beforeID;
protected function getPagingColumn() {
return 'id';
}
protected function getPagingValue($result) {
return $result->getID();
}
protected function nextPage(array $page) {
if ($this->beforeID) {
$this->beforeID = $this->getPagingValue(head($page));
} else {
$this->afterID = $this->getPagingValue(last($page));
}
}
final public function setAfterID($object_id) {
$this->afterID = $object_id;
return $this;
}
final public function setBeforeID($object_id) {
$this->beforeID = $object_id;
return $this;
}
final protected function buildLimitClause(AphrontDatabaseConnection $conn_r) {
if ($this->getLimit()) {
return qsprintf($conn_r, 'LIMIT %d', $this->getLimit());
} else {
return '';
}
}
final protected function buildPagingClause(
AphrontDatabaseConnection $conn_r) {
if ($this->beforeID) {
return qsprintf(
$conn_r,
'%C > %s',
$this->getPagingColumn(),
$this->beforeID);
} else if ($this->afterID) {
return qsprintf(
$conn_r,
'%C < %s',
$this->getPagingColumn(),
$this->afterID);
}
return null;
}
final protected function buildOrderClause(AphrontDatabaseConnection $conn_r) {
if ($this->beforeID) {
return qsprintf(
$conn_r,
'ORDER BY %C ASC',
$this->getPagingColumn());
} else {
return qsprintf(
$conn_r,
'ORDER BY %C DESC',
$this->getPagingColumn());
}
}
final protected function processResults(array $results) {
if ($this->beforeID) {
$results = array_reverse($results, $preserve_keys = true);
}
return $results;
}
final public function executeWithPager(AphrontIDPagerView $pager) {
$this->setLimit($pager->getPageSize() + 1);
if ($pager->getAfterID()) {
$this->setAfterID($pager->getAfterID());
} else if ($pager->getBeforeID()) {
$this->setBeforeID($pager->getBeforeID());
}
$results = $this->execute();
$sliced_results = $pager->sliceResults($results);
if ($this->beforeID || (count($results) > $pager->getPageSize())) {
$pager->setNextPageID($this->getPagingValue(last($sliced_results)));
}
if ($this->afterID ||
($this->beforeID && (count($results) > $pager->getPageSize()))) {
$pager->setPrevPageID($this->getPagingValue(head($sliced_results)));
}
return $sliced_results;
}
}

View file

@ -0,0 +1,15 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'infrastructure/query/policy/base');
phutil_require_module('phabricator', 'storage/qsprintf');
phutil_require_module('phutil', 'utils');
phutil_require_source('PhabricatorIDPagedPolicyQuery.php');

View file

@ -0,0 +1,142 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class AphrontIDPagerView extends AphrontView {
private $afterID;
private $beforeID;
private $pageSize = 100;
private $nextPageID;
private $prevPageID;
private $uri;
final public function setPageSize($page_size) {
$this->pageSize = max(1, $page_size);
return $this;
}
final public function getPageSize() {
return $this->pageSize;
}
final public function setURI(PhutilURI $uri) {
$this->uri = $uri;
return $this;
}
final public function readFromRequest(AphrontRequest $request) {
$this->uri = $request->getRequestURI();
$this->afterID = $request->getStr('after');
$this->beforeID = $request->getStr('before');
return $this;
}
final public function setAfterID($after_id) {
$this->afterID = $after_id;
return $this;
}
final public function getAfterID() {
return $this->afterID;
}
final public function setBeforeID($before_id) {
$this->beforeID = $before_id;
return $this;
}
final public function getBeforeID() {
return $this->beforeID;
}
final public function setNextPageID($next_page_id) {
$this->nextPageID = $next_page_id;
return $this;
}
final public function getNextPageID() {
return $this->nextPageID;
}
final public function setPrevPageID($prev_page_id) {
$this->prevPageID = $prev_page_id;
return $this;
}
final public function getPrevPageID() {
return $this->prevPageID;
}
final public function sliceResults(array $results) {
if (count($results) > $this->getPageSize()) {
$results = array_slice($results, 0, $this->getPageSize(), true);
}
return $results;
}
public function render() {
if (!$this->uri) {
throw new Exception(
"You must call setURI() before you can call render().");
}
$links = array();
if ($this->beforeID || $this->afterID) {
$links[] = phutil_render_tag(
'a',
array(
'href' => $this->uri
->alter('before', null)
->alter('after', null),
),
"\xC2\xAB First");
}
if ($this->prevPageID) {
$links[] = phutil_render_tag(
'a',
array(
'href' => $this->uri
->alter('after', null)
->alter('before', $this->prevPageID),
),
"\xE2\x80\xB9 Prev");
}
if ($this->nextPageID) {
$links[] = phutil_render_tag(
'a',
array(
'href' => $this->uri
->alter('after', $this->nextPageID)
->alter('before', null),
),
"Next \xE2\x80\xBA");
}
return
'<div class="aphront-pager-view">'.
implode('', $links).
'</div>';
}
}

View file

@ -0,0 +1,14 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'view/base');
phutil_require_module('phutil', 'markup');
phutil_require_source('AphrontIDPagerView.php');

View file

@ -0,0 +1,52 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class AphrontFormPolicyControl extends AphrontFormControl {
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function getUser() {
return $this->user;
}
protected function getCustomControlClass() {
return 'aphront-form-control-policy';
}
private function getOptions() {
return array(
PhabricatorPolicies::POLICY_USER => 'All Users',
);
}
protected function renderInput() {
return AphrontFormSelectControl::renderSelectTag(
$this->getValue(),
$this->getOptions(),
array(
'name' => $this->getName(),
'disabled' => $this->getDisabled() ? 'disabled' : null,
'id' => $this->getID(),
));
}
}

View file

@ -0,0 +1,14 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'applications/policy/constants/policy');
phutil_require_module('phabricator', 'view/form/control/base');
phutil_require_module('phabricator', 'view/form/control/select');
phutil_require_source('AphrontFormPolicyControl.php');

View file

@ -89,3 +89,7 @@
padding-bottom: .5em;
margin-bottom: .5em;
}
.aphront-access-dialog {
width: 50%;
}