Details
- Reviewers
epriestley - Group Reviewers
Blessed Reviewers - Maniphest Tasks
- T7703: Policy checks may execute incompletely for non-viewers
T8352: Link Diviner books to repositories - Commits
- Restricted Diffusion Commit
rP69d12f64baf9: Add repositories to Diviner
Ran diviner generate --repository ARC and then went to /diviner/book/arcanist/.
Diff Detail
- Repository
- rP Phabricator
- Branch
- master
- Lint
Lint Passed - Unit
Tests Passed - Build Status
Buildable 6396 Build 6418: [Placeholder Plan] Wait for 30 Seconds
Event Timeline
Per T8352, I think this should be nullable.
src/applications/diviner/query/DivinerBookQuery.php | ||
---|---|---|
30–33 | I think we should always load these and perform policy filtering; if you can't see a repository, you should not be able to see its books (similar to how revisions work). (This might create some issues with generating public documentation from private repositories, but I don't think that's a major use case and it shouldn't be the default behavior.) | |
45–47 | For consistency, do this in willFilterPage(). | |
96 | Prefer !== null, so setRepositoryPHIDs($phids) is an error when $phids is array(), instead of an accidental query for all results. | |
126 | If the repository fails to load, I think we should filter the book out (i.e., same deal as revisions: if you can't see a repository, you can't see its stuff). | |
src/applications/diviner/workflow/DivinerGenerateWorkflow.php | ||
28–32 | I think this should be in the .book file instead, possibly with an optional flag to override it. But bin/diviner generate shouldn't need flags to do the right thing, and you should theoretically be able to generate multiple books across different repositories with one command. |
src/applications/diviner/workflow/DivinerGenerateWorkflow.php | ||
---|---|---|
28–32 | Yeah, this was just easier for to me implement for now... I was thinking that we should try to determine the repository using the following methods:
|
src/applications/diviner/controller/DivinerBookController.php | ||
---|---|---|
41–47 | Not sure if this is valuable/necessary... I primarily added it so that I could verify that this was working. | |
src/applications/diviner/publisher/DivinerLivePublisher.php | ||
7–26 | This seems really gross and I might try to clean it up. | |
16 | Maybe we should set this to the view policy of the repository? | |
34 | This is unrelated, but I think that this was wrong originally? |
- Allow repositoryPHID to be nullable
- Always load repositories
- Move repository loading to willFilterPage
- Use $repositoryPHID !== null
- Filter books that cannot be viewed due to policies
- Fix indentation
- Tidy up DivinerLivePublisher::loadBook method
- Set book view policy from repository
- Rename DivinerLivePublisher::loadBook to DivinerLivePublisher::getBook
- Allow repository to be specified in book file
Couple of inlines but this looks good to me.
src/applications/diviner/publisher/DivinerLivePublisher.php | ||
---|---|---|
12–21 | We should just enforce it implicitly, like with Differential revisions. The describeAutomaticCapability() method of DivinerLiveBook should be updated to explain this, though (see DifferentialRevision). | |
34 | This looks wrong now, since it passes a Book object to a with...PHIDs() method? (The original code seems more wrong, but I think there's still a getPHID() missing.) | |
src/applications/diviner/query/DivinerBookQuery.php | ||
56 | This likely needs to $book->attachRepository(null) to avoid throwing when loading books with no repository and later calling getRepository() on them. (That will imply a corresponding PhabricatorRepository $repository = null default parameter value in attachRepository().) | |
src/applications/diviner/storage/DivinerLiveBook.php | ||
23 | I'd expect this to need a 'repositoryPHID' => 'phid?' to avoid a bin/storage upgrade warning about column nullability. | |
src/applications/repository/storage/PhabricatorRepository.php | ||
1204 | This should be $engine->destroyObject($book) and be fired from destroyObjectPermanently(), not delete(). |
Your comments seem accurate, I'm heading out shortly but will fix this up later. I will probably bounce back to you for review since I haven't touched some of this code before.
src/applications/diviner/publisher/DivinerLivePublisher.php | ||
---|---|---|
12–21 | And then not set a view policy on the DivinerLiveBook at all? | |
34 | Oh you're right about the missing getPHID. It's confusing because some classes (such as DivinerAtom) have getBook() and setBook() methods which handle PHIDs rather than book objects. |
src/applications/diviner/publisher/DivinerLivePublisher.php | ||
---|---|---|
34 | Actually yeah... in the DivinerAtom class, $book is a PHID, not an instance of DivinerLiveBook. |
- Remove --repository argument from DivinerGenerateWorkflow
- Rename a variable
- Allow repository to be null
Pushing this back for review... a couple of questions:
- Should I attempt to automatically determine the repository from the working copy?
- Should repositoryPHID be an attribute of DivinerLiveAtom or DivinerLiveSymbol?
- Did I do the automatic capabilities properly? Not sure how to test this.
src/applications/diviner/workflow/DivinerGenerateWorkflow.php | ||
---|---|---|
28–32 | I'm actually not sure about this anymore... in D13091 I add the functionality to change the repository from the web UI, but maybe this doesn't make sense? I think that, ideally, the repository would be automatically determined from the working copy. If that doesn't work then I guess we could maybe allow the repository to be specified in the .book file. Is this sufficient? Do we need any further customizations? |
Should I attempt to automatically determine the repository from the working copy?
I probably wouldn't bother for now. I think the code Arcanist uses to do this is nontrivial, and we might get it free later if more of this ends up in Arcanist.
Should repositoryPHID be an attribute of DivinerLiveAtom or DivinerLiveSymbol?
It should be a property of DivinerLiveSymbol (so we can query for it -- all the user-facing query UI operates on Symbols). For technical/internal reasons, it might also end up somewhere on LiveAtom (either as a property or in a JSON blob somewhere), but that would just be to make getting it onto Symbol easier.
Did I do the automatic capabilities properly? Not sure how to test this.
It's correct as written, or you could just remove it for now. Lengthy reasoning and then relevant discussion:
"Automatic Capabilities" are a formal concept which allow users with a strong connection to an object (like a task owner, or revision author) to always have a capability, even if the policy says they don't. For example, a revision with "Edit Policy: No One" really means "Edit Policy: No One (Except the Author)". Automatic capabilities always weaken policies, and let users who do not pass policy checks take actions because of a strong relationship to the object.
"Implicit Policies" aren't really formal right now, although there's a reasonable chance that they'll become formal in order to fix T7703 (maybe as "Extended Policies"). For some kinds of objects, you need to be able to see or edit some related objects in order to see or edit a target object. For example, in Differential, you must be able to see the repository a revision is associated with in order to see the revision. In Phriction, you need view/edit capabilities on all parents of a document. In Phame, you (probably?) need to be able to see a blog in order to see posts on it. In a few other cases there are more infrastructure-level implicit capabilities: for example, you must be able to see an event in order to see its invitees.
Currently, these are implemented as (a) a "load, attach, discard records with no corresponding object" step in the Query, and (b) a human-readable note in describeAutomaticCapability(). This is somewhat confusing because the note isn't describing an automatic capability at all, but it puts the information in the right place in the UI and it wasn't a case I was thinking about when I wrote the method. A better name for this method might just be describeCapability() or describeSpecialRulesForCapability().
After T7703, they will probably also be implemented as an explicit step in PolicyFilter. Specifically, we would probably add some method like this after T7703:
public function getExtendedPolicyRequirements($capability) { return array( // Some datastructure which means "you must be able to // see the repository to see the book". ); }
These implicit policies always strengthen policies: they prevent users who would otherwise pass the policy check from taking actions.
After T3820, Spaces will also essentially put an implicit policy layer on top of all objects. So policy evaluation will roughly be:
(<Viewer Can See Space> AND <Viewer Can See Implicit Related Objects> AND (<Viewer Satisfies Policy> OR <Viewer Has Automatic Capability>))
So, in this specific case, the automatic capability implementation is correct, but it's also moot, and it would be equally correct if removed: repositories do not have any automatic capabilities, because no users have an exceptionally strong relationship to repositories: they aren't currently "owned" objects. If we gave some users a strong relationship to repositories in the future, it might or might not make sense for that relationship to also imply an automatic capability on books.
For example, if the relationship was "ownership", it might be reasonable to say that repository owners can always see books in their repository.
If the relationship was something like "permission to build/mirror" to support some kind of Harbormaster thing (I'm just making this up as an example, I can't really think of a good one), it might not make sense for that relationship to extend to books.
I'd maybe slightly favor just removing the code. Although "ownership" is by far the most likely automatic capability we'd add in the future and the code would be correct/necessary if we did, it does nothing today, I don't currently plan to add "ownership", and the code might cause us to apply too liberal a policy if we implement some other kind of automatic capability. Even if we implement "ownership", an explicit test to see if the viewer is a repository owner might be a better fix.
(There's also a third kind of implicit/indirect policy relationship, where completely-dependent sub-objects just directly reflect the policies of their parent objects. An example of this is Changesets in Differential. It never makes sense for a Changeset to have any policy other than the policy of its parent Diff, so the Changeset has no viewPolicy/editPolicy fields and just proxies Diff for the purposes of policy checks. This kind of object is straightforward and not affected by T7703, because it isn't adding any policy checks of its own. In this case, it's appropriate to just call through to the parent/container object.)
src/applications/diviner/publisher/DivinerLivePublisher.php | ||
---|---|---|
12–22 | Right. In a future diff, the new DivinerLiveBook() can become DivinerLiveBook::initializeNewBook() which would read the default application view/edit policies. | |
34 | Oh, yuck. We should maybe fix this at some point, although that might be involved. | |
src/applications/diviner/storage/DivinerLiveBook.php | ||
92–93 | (See discussion.) |
I think it makes sense to leave the automatic capability... it seems logical that access to view the repository implies access to view the documentation.
src/applications/diviner/publisher/DivinerLivePublisher.php | ||
---|---|---|
29 | We'll have to revisit this at some point if we build arc diviner or similar, but it should be fine for now. | |
src/applications/diviner/query/DivinerBookQuery.php | ||
47 | This will technically work fine, but mpull($books, 'getRepositoryPHID') might return something like array(null, null, null). This might be slightly clearer (although also slightly longer) with more effort to handle that case. | |
61–62 | Per that recent handle stuff, should didRejectResult() here. |
I had a thought... we could use the phabricator_repository.repository_path and phabricator_repository.repository_pathchange tables in order to determine the repository based on the path to the book. Thoughts?
We could, but that seems like a lot of effort for not much benefit to me. I think the existing mechanisms in arc (primarily, remote URI) are much better.
If you wanted to hack something in now, it's probably a comparable amount of effort to instantiate an Arcanist WorkingCopy object and then directly use the code to resolve the repository than try to do the path stuff (although this might be more complicated than I believe).
I'll defer until a later diff... I'm just going to move it to an argument (diviner generate --repository $callsign) for now.
Almost completely working. The only issue at the moment is that running diviner generate --repository X does not update the repository for the Diviner atoms if they had already previously been published.
@epriestley, do you have any thoughts on how to go about this? I was thinking of just adding queryfx($conn_w, 'UPDATE %T SET repositoryPHID = %s WHERE bookPHID = %s', id(new DivinerLiveSymbol())->getTableName(), $book->getRepositoryPHID(), $book->getPHID()) to DivinerLivePublisher::loadBook(), but that's pretty gross.
src/applications/diviner/controller/DivinerBookEditController.php | ||
---|---|---|
85–87 ↗ | (On Diff #32300) | This seems really gross |
src/applications/diviner/controller/DivinerBookEditController.php | ||
---|---|---|
85–87 ↗ | (On Diff #32319) | Yeah, we end up doing this in a fair number of places. It would probably be OK to just let you setValue() a string and convert it into an array (only if the limit is 1?), or maybe add setSingleValue()? We could clean up a few things. |
For the atom stuff, I thiiink there's probably some global/book cachekey we can put the repo PHID into to automatically invalidate everything, but we might need to add that. Hack fix seems fine for now.