1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-10 00:42:41 +01:00

Add basic support for editing project policies

Summary:
This case is unusually complicated because there are more rules than most objects will have.

  - Edits are either "joins", "leaves" or "other edits".
  - "Joins" require "can join" or "can edit".
  - "Leaves" don't require any policy.
  - "Other edits" require "can edit".
  - You can't edit away your ability to edit.
  - You //can// leave a project that you wouldn't be able to rejoin.

Things I'm going to add:

  - Global log of policy changes.
  - `bin/policy` script for undoing policy changes.
  - Test coverage for these rules.

Test Plan: Made various project visibility edits with various users, joined / left projects, etc. I'll add more complete coverage in the next diff.

Reviewers: btrahan, vrana

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T603

Differential Revision: https://secure.phabricator.com/D3270
This commit is contained in:
epriestley 2012-08-15 10:44:58 -07:00
parent 51c5a9b067
commit 42461b5f06
7 changed files with 253 additions and 24 deletions

View file

@ -22,5 +22,8 @@ final class PhabricatorProjectTransactionType
const TYPE_NAME = 'name'; const TYPE_NAME = 'name';
const TYPE_MEMBERS = 'members'; const TYPE_MEMBERS = 'members';
const TYPE_STATUS = 'status'; const TYPE_STATUS = 'status';
const TYPE_CAN_VIEW = 'canview';
const TYPE_CAN_EDIT = 'canedit';
const TYPE_CAN_JOIN = 'canjoin';
} }

View file

@ -1,7 +1,7 @@
<?php <?php
/* /*
* Copyright 2011 Facebook, Inc. * Copyright 2012 Facebook, Inc.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -53,9 +53,28 @@ abstract class PhabricatorProjectController extends PhabricatorController {
$nav_view->addFilter(null, 'Wiki '.$external_arrow, $phriction_uri); $nav_view->addFilter(null, 'Wiki '.$external_arrow, $phriction_uri);
$nav_view->addFilter('people', 'People'); $nav_view->addFilter('people', 'People');
$nav_view->addFilter('about', 'About'); $nav_view->addFilter('about', 'About');
$user = $this->getRequest()->getUser();
$can_edit = PhabricatorPolicyCapability::CAN_EDIT;
$nav_view->addSpacer(); $nav_view->addSpacer();
$nav_view->addFilter('edit', "Edit Project\xE2\x80\xA6", $edit_uri); if (PhabricatorPolicyFilter::hasCapability($user, $project, $can_edit)) {
$nav_view->addFilter('members', "Edit Members\xE2\x80\xA6", $members_uri); $nav_view->addFilter('edit', "Edit Project\xE2\x80\xA6", $edit_uri);
$nav_view->addFilter('members', "Edit Members\xE2\x80\xA6", $members_uri);
} else {
$nav_view->addFilter(
'edit',
"Edit Project\xE2\x80\xA6",
$edit_uri,
$relative = false,
'disabled');
$nav_view->addFilter(
'members',
"Edit Members\xE2\x80\xA6",
$members_uri,
$relative = false,
'disabled');
}
return $nav_view; return $nav_view;
} }

View file

@ -27,7 +27,15 @@ final class PhabricatorProjectMembersEditController
$request = $this->getRequest(); $request = $this->getRequest();
$user = $request->getUser(); $user = $request->getUser();
$project = id(new PhabricatorProject())->load($this->id); $project = id(new PhabricatorProjectQuery())
->setViewer($user)
->withIDs(array($this->id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$project) { if (!$project) {
return new Aphront404Response(); return new Aphront404Response();
} }

View file

@ -31,24 +31,30 @@ final class PhabricatorProjectProfileController
$request = $this->getRequest(); $request = $this->getRequest();
$user = $request->getUser(); $user = $request->getUser();
$project = id(new PhabricatorProject())->load($this->id); $query = id(new PhabricatorProjectQuery())
->setViewer($user)
->withIDs(array($this->id));
if ($this->page == 'people') {
$query->needMembers(true);
}
$project = $query->executeOne();
if (!$project) { if (!$project) {
return new Aphront404Response(); return new Aphront404Response();
} }
$profile = $project->loadProfile(); $profile = $project->loadProfile();
if (!$profile) { if (!$profile) {
$profile = new PhabricatorProjectProfile(); $profile = new PhabricatorProjectProfile();
} }
$picture = $profile->loadProfileImageURI(); $picture = $profile->loadProfileImageURI();
$members = $project->loadMemberPHIDs();
$member_map = array_fill_keys($members, true);
$nav_view = $this->buildLocalNavigation($project); $nav_view = $this->buildLocalNavigation($project);
$this->page = $nav_view->selectFilter($this->page, 'dashboard'); $this->page = $nav_view->selectFilter($this->page, 'dashboard');
require_celerity_resource('phabricator-profile-css'); require_celerity_resource('phabricator-profile-css');
switch ($this->page) { switch ($this->page) {
case 'dashboard': case 'dashboard':
@ -88,7 +94,15 @@ final class PhabricatorProjectProfileController
$header->setProfilePicture($picture); $header->setProfilePicture($picture);
$action = null; $action = null;
if (empty($member_map[$user->getPHID()])) { if (!$project->isUserMember($user->getPHID())) {
$can_join = PhabricatorPolicyCapability::CAN_JOIN;
if (PhabricatorPolicyFilter::hasCapability($user, $project, $can_join)) {
$class = 'green';
} else {
$class = 'grey disabled';
}
$action = phabricator_render_form( $action = phabricator_render_form(
$user, $user,
array( array(
@ -98,7 +112,7 @@ final class PhabricatorProjectProfileController
phutil_render_tag( phutil_render_tag(
'button', 'button',
array( array(
'class' => 'green', 'class' => $class,
), ),
'Join Project')); 'Join Project'));
} else { } else {
@ -172,7 +186,7 @@ final class PhabricatorProjectProfileController
PhabricatorProject $project, PhabricatorProject $project,
PhabricatorProjectProfile $profile) { PhabricatorProjectProfile $profile) {
$member_phids = $project->loadMemberPHIDs(); $member_phids = $project->getMemberPHIDs();
$handles = id(new PhabricatorObjectHandleData($member_phids)) $handles = id(new PhabricatorObjectHandleData($member_phids))
->loadHandles(); ->loadHandles();

View file

@ -28,10 +28,19 @@ final class PhabricatorProjectProfileEditController
$request = $this->getRequest(); $request = $this->getRequest();
$user = $request->getUser(); $user = $request->getUser();
$project = id(new PhabricatorProject())->load($this->id); $project = id(new PhabricatorProjectQuery())
->setViewer($user)
->withIDs(array($this->id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$project) { if (!$project) {
return new Aphront404Response(); return new Aphront404Response();
} }
$profile = $project->loadProfile(); $profile = $project->loadProfile();
if (empty($profile)) { if (empty($profile)) {
$profile = new PhabricatorProjectProfile(); $profile = new PhabricatorProjectProfile();
@ -62,6 +71,24 @@ final class PhabricatorProjectProfileEditController
$xaction->setNewValue($request->getStr('status')); $xaction->setNewValue($request->getStr('status'));
$xactions[] = $xaction; $xactions[] = $xaction;
$xaction = new PhabricatorProjectTransaction();
$xaction->setTransactionType(
PhabricatorProjectTransactionType::TYPE_CAN_VIEW);
$xaction->setNewValue($request->getStr('can_view'));
$xactions[] = $xaction;
$xaction = new PhabricatorProjectTransaction();
$xaction->setTransactionType(
PhabricatorProjectTransactionType::TYPE_CAN_EDIT);
$xaction->setNewValue($request->getStr('can_edit'));
$xactions[] = $xaction;
$xaction = new PhabricatorProjectTransaction();
$xaction->setTransactionType(
PhabricatorProjectTransactionType::TYPE_CAN_JOIN);
$xaction->setNewValue($request->getStr('can_join'));
$xactions[] = $xaction;
$editor = new PhabricatorProjectEditor($project); $editor = new PhabricatorProjectEditor($project);
$editor->setUser($user); $editor->setUser($user);
$editor->applyTransactions($xactions); $editor->applyTransactions($xactions);
@ -150,6 +177,31 @@ final class PhabricatorProjectProfileEditController
->setLabel('Blurb') ->setLabel('Blurb')
->setName('blurb') ->setName('blurb')
->setValue($profile->getBlurb())) ->setValue($profile->getBlurb()))
->appendChild(
'<p class="aphront-form-instructions">NOTE: Policy settings are not '.
'yet fully implemented. Some interfaces still ignore these settings, '.
'particularly "Visible To".</p>')
->appendChild(
id(new AphrontFormPolicyControl())
->setUser($user)
->setName('can_view')
->setCaption('Members can always view a project.')
->setPolicyObject($project)
->setCapability(PhabricatorPolicyCapability::CAN_VIEW))
->appendChild(
id(new AphrontFormPolicyControl())
->setUser($user)
->setName('can_edit')
->setPolicyObject($project)
->setCapability(PhabricatorPolicyCapability::CAN_EDIT))
->appendChild(
id(new AphrontFormPolicyControl())
->setUser($user)
->setName('can_join')
->setCaption(
'Users who can edit a project can always join a project.')
->setPolicyObject($project)
->setCapability(PhabricatorPolicyCapability::CAN_JOIN))
->appendChild( ->appendChild(
id(new AphrontFormMarkupControl()) id(new AphrontFormMarkupControl())
->setLabel('Profile Image') ->setLabel('Profile Image')

View file

@ -31,18 +31,14 @@ final class PhabricatorProjectUpdateController
$request = $this->getRequest(); $request = $this->getRequest();
$user = $request->getUser(); $user = $request->getUser();
$project = id(new PhabricatorProjectQuery()) $capabilities = array(
->setViewer($user) PhabricatorPolicyCapability::CAN_VIEW,
->needMembers(true) );
->withIDs(array($this->id))
->executeOne();
if (!$project) {
return new Aphront404Response();
}
$process_action = false; $process_action = false;
switch ($this->action) { switch ($this->action) {
case 'join': case 'join':
$capabilities[] = PhabricatorPolicyCapability::CAN_JOIN;
$process_action = $request->isFormPost(); $process_action = $request->isFormPost();
break; break;
case 'leave': case 'leave':
@ -52,6 +48,16 @@ final class PhabricatorProjectUpdateController
return new Aphront404Response(); return new Aphront404Response();
} }
$project = id(new PhabricatorProjectQuery())
->setViewer($user)
->withIDs(array($this->id))
->needMembers(true)
->requireCapabilities($capabilities)
->executeOne();
if (!$project) {
return new Aphront404Response();
}
$project_uri = '/project/view/'.$project->getID().'/'; $project_uri = '/project/view/'.$project->getID().'/';
if ($process_action) { if ($process_action) {

View file

@ -95,22 +95,54 @@ final class PhabricatorProjectEditor {
} }
foreach ($transactions as $key => $xaction) { foreach ($transactions as $key => $xaction) {
$type = $xaction->getTransactionType();
$this->setTransactionOldValue($project, $xaction); $this->setTransactionOldValue($project, $xaction);
if (!$this->transactionHasEffect($xaction)) { if (!$this->transactionHasEffect($xaction)) {
unset($transactions[$key]); unset($transactions[$key]);
continue; continue;
} }
}
$this->applyTransactionEffect($project, $xaction); if (!$is_new) {
// You must be able to view a project in order to edit it in any capacity.
PhabricatorPolicyFilter::requireCapability(
$user,
$project,
PhabricatorPolicyCapability::CAN_VIEW);
$need_edit = false;
$need_join = false;
foreach ($transactions as $key => $xaction) {
if ($this->getTransactionRequiresEditCapability($xaction)) {
$need_edit = true;
}
if ($this->getTransactionRequiresJoinCapability($xaction)) {
$need_join = true;
}
}
if ($need_edit) {
PhabricatorPolicyFilter::requireCapability(
$user,
$project,
PhabricatorPolicyCapability::CAN_EDIT);
}
if ($need_join) {
PhabricatorPolicyFilter::requireCapability(
$user,
$project,
PhabricatorPolicyCapability::CAN_JOIN);
}
} }
if (!$transactions) { if (!$transactions) {
return $this; return $this;
} }
foreach ($transactions as $xaction) {
$this->applyTransactionEffect($project, $xaction);
}
try { try {
$project->openTransaction(); $project->openTransaction();
$project->save(); $project->save();
@ -203,6 +235,15 @@ final class PhabricatorProjectEditor {
$new_value = array_values($new_value); $new_value = array_values($new_value);
$xaction->setNewValue($new_value); $xaction->setNewValue($new_value);
break; break;
case PhabricatorProjectTransactionType::TYPE_CAN_VIEW:
$xaction->setOldValue($project->getViewPolicy());
break;
case PhabricatorProjectTransactionType::TYPE_CAN_EDIT:
$xaction->setOldValue($project->getEditPolicy());
break;
case PhabricatorProjectTransactionType::TYPE_CAN_JOIN:
$xaction->setOldValue($project->getJoinPolicy());
break;
default: default:
throw new Exception("Unknown transaction type '{$type}'!"); throw new Exception("Unknown transaction type '{$type}'!");
} }
@ -229,6 +270,21 @@ final class PhabricatorProjectEditor {
$this->addEdges = array_keys(array_diff_key($new, $old)); $this->addEdges = array_keys(array_diff_key($new, $old));
$this->remEdges = array_keys(array_diff_key($old, $new)); $this->remEdges = array_keys(array_diff_key($old, $new));
break; break;
case PhabricatorProjectTransactionType::TYPE_CAN_VIEW:
$project->setViewPolicy($xaction->getNewValue());
break;
case PhabricatorProjectTransactionType::TYPE_CAN_EDIT:
$project->setEditPolicy($xaction->getNewValue());
// You can't edit away your ability to edit the project.
PhabricatorPolicyFilter::mustRetainCapability(
$this->user,
$project,
PhabricatorPolicyCapability::CAN_EDIT);
break;
case PhabricatorProjectTransactionType::TYPE_CAN_JOIN:
$project->setJoinPolicy($xaction->getNewValue());
break;
default: default:
throw new Exception("Unknown transaction type '{$type}'!"); throw new Exception("Unknown transaction type '{$type}'!");
} }
@ -264,4 +320,75 @@ final class PhabricatorProjectEditor {
return ($xaction->getOldValue() !== $xaction->getNewValue()); return ($xaction->getOldValue() !== $xaction->getNewValue());
} }
/**
* All transactions except joining or leaving a project require edit
* capability.
*/
private function getTransactionRequiresEditCapability(
PhabricatorProjectTransaction $xaction) {
return ($this->isJoinOrLeaveTransaction($xaction) === null);
}
/**
* Joining a project requires the join capability. Anyone leave a project.
*/
private function getTransactionRequiresJoinCapability(
PhabricatorProjectTransaction $xaction) {
$type = $this->isJoinOrLeaveTransaction($xaction);
return ($type == 'join');
}
/**
* Returns 'join' if this transaction causes the acting user ONLY to join the
* project.
*
* Returns 'leave' if this transaction causes the acting user ONLY to leave
* the project.
*
* Returns null in all other cases.
*/
private function isJoinOrLeaveTransaction(
PhabricatorProjectTransaction $xaction) {
$type = $xaction->getTransactionType();
if ($type != PhabricatorProjectTransactionType::TYPE_MEMBERS) {
return null;
}
switch ($type) {
case PhabricatorProjectTransactionType::TYPE_MEMBERS:
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
$add = array_diff($new, $old);
$rem = array_diff($old, $new);
if (count($add) > 1) {
return null;
} else if (count($add) == 1) {
if (reset($add) != $this->user->getPHID()) {
return null;
} else {
return 'join';
}
}
if (count($rem) > 1) {
return null;
} else if (count($rem) == 1) {
if (reset($rem) != $this->user->getPHID()) {
return null;
} else {
return 'leave';
}
}
break;
}
return true;
}
} }