RE: Modeling permissions with Hydra

On 29 Okt 2015 at 16:32, Ryan Shaw wrote:
> We have an API for managing scholarly notes. The API is organized as
> follows: a scholarly project has a collection of notes associated with
> it. Projects may have multiple users, each of which may have different
> role-based permissions to do things with the project's notes. The
> names and permissions of these roles are project-specific. Users may
> belong to multiple projects and have different roles in each.
> 
> One of the main reasons we chose to use Hydra is to help clients
> understand what they are permitted to do with the various resources
> they have access to. So, our entry point to the API should show an
> authenticated user links to the note collections of all the projects
> with which the user is affiliated, and the client should be able to
> determine, e.g., which of these collections can be written to, and
> which can only be read.
> 
> If I understand correctly, there are several approaches we could take.
> (The thread on "Specifying operations for instances" [1] was critical
> for helping me understand this, by the way---the current version of
> the spec does not make these alternatives very clear.)

Yeah the spec is one thing that has to be improved. I would like to have dedicated articles for common questions like these which present the options and discuss their pros and cons. At the moment we have no such articles. I'd love to put this on our website if you have time to write it down. It shouldn't look like a specification but more like a tutorial or a typical blog post. We can add non-normative references from the spec directly to those articles discussing certain aspects in more detail.


> For those
> readers that, like me, were unclear on the differences among these
> approaches, I've gone into pedantic detail descibing them below, but

Indeed :-) It's great material that we could hopefully reuse though. Did you try again to join the CG? It's kind of important that you agree to the terms so that we don't run into problems down the road.


> we might summarize them as follows:
> 
> 1. The first choice to be made is whether we want to specify supported
> operations by describing resource classes, by describing links, or by
> describing individual resources.
> 
> 2. If we choose to describe classes or links, we need to further decide
> whether we will choose to communicate permissions by keeping the API
> documentation relatively stable and varying representations, or by
> keeping representations relatively stable and varying API documentation.
> 
> 3. A third decision to be made is the extent to which we expose or
> model the workings of the permissions system: ought we make clients
> aware that there are such things as user roles, for example by
> defining links based on roles, or should we simply say "these are the
> supported operations of this link" and hide the fact that those
> operations are being determined by the role of the user?
> 
> Of course these are not mutually exclusive choices---we could take
> hybrid approaches---but I think this covers the basic dimensions of
> the design space. Currently we have adopted what seems to be the
> simplest option: describing individual related resources in our
> representations (approach 3 below). This is a good fit for web
> frameworks providing functions which will take a user and an object
> and give us back a set of permissions the user has with respect to
> that object, which can then be serialized as the objects of
> `hydra:operation` statements.

Yep, that's what websites do in general as well. My typical advice to people is to look at what websites do an replicate that. Most web sites are better Web APIs than what we commonly call Web APIs.


> But I am wondering what others' experience has been with doing this.
> What approaches have you taken? What do you see as the pros and cons?
> Are there possible approaches I haven't identified? One issue that

One approach that you haven't considered is given clients all the options but telling them at run time which aren't possible. In other words, responding with a 401 Unauthorized. I do that in the demo APIs for instance when users aren't authenticated.


> seems relevant is cacheability. Is it better to have API documentation
> that varies infrequently, or specific resource representations that
> change infrequently?

For me personally, it feels more natural to have a ApiDocumentation that can be cached for a long time than optimizing for resource cacheability.


> Are there other considerations? Finally, if there
> aren't concrete benefits to one approach over another, do we need so
> many different ways to achieve basically the same thing?

It's a side effect of how things work that there are multiple options. It's not that we deliberately wanted or needed all of them.


Cheers,
Markus



> [1] https://lists.w3.org/Archives/Public/public-hydra/2014Jun/0054.html
> 
> ----
> 
> 1. Describe resource classes
> 
> 
> 1A. Define instances of `hydra:Class` with different supported
> operations, and use these to type resources. So, in our API
> documentation we could have:
> 
> ex:Note a hydra:Class .
> 
> ex:NoteCollection
>     a hydra:Class ;
>     hydra:supportedOperation [
>         a hydra:Operation ;
>         hydra:method "GET" ;
>         hydra:returns ex:NoteCollection
>     ] .
> ex:EditableNoteCollection
>     a hydra:Class ;
>     hydra:supportedOperation [
>         a hydra:CreateResourceOperation ;
>         hydra:method "POST" ;
>         hydra:expects ex:Note ;
>         hydra:returns ex:Note
>     ] .
> ex:notes
>     a hydra:Link ;
>     rdfs:range ex:NoteCollection .
> Then, the representation of the entry point for a user A who has
> permission to add notes to project 1, but not to project 2, would be
> something like:
> 
> <>
>     ex:notes <project/1/notes/> ;
>     ex:notes <project/2/notes/> .
> <project/1/notes> a ex:EditableNoteCollection .
> 
> <project/2/notes> a ex:NoteCollection .
> 
> Taking this approach, the API documentation will remain relatively
> static, and the types assigned to resources in representations will
> change dynamically as permissions change.
> 
> 
> 1B. Define different instances of `hydra:Class` for different
> projects, and use these to type resources. This would involve defining
> different "sub-APIs" per project, and varying the documentation of
> these based on permissions. So, the documentation that user A would
> see for the API to project 1 would include:
> 
> ex:Note a hydra:Class .
> 
> ex-project1:NoteCollection
>     a hydra:Class ;
>     hydra:supportedOperation [
>         a hydra:Operation ; hydra:method "GET" ; hydra:returns
>         ex-project1:NoteCollection ], [ a hydra:CreateResourceOperation
>         ; hydra:method "POST" ; hydra:expects ex:Note ; hydra:returns
>         ex:Note
>     ] .
> The documentation that User A would see for the API to project 2 would include:
> 
> ex-project2:NoteCollection
>     a hydra:Class ;
>     hydra:supportedOperation [
>         a hydra:Operation ;
>         hydra:method "GET" ;
>         hydra:returns ex-project2:NoteCollection
>     ] .
> And the entry point representation:
> 
> <>
>     ex:notes <project/1/notes/> ;
>     ex:notes <project/2/notes/> .
> <project/1/notes> a ex-project1:NoteCollection .
> 
> <project/2/notes> a ex-project2:NoteCollection .
> 
> ex:notes a hydra:Link .
> 
> Taking this approach, the API documentation for individual projects
> would change dynamically, while the representation of the entry point
> would be relatively static.
> 
> (Incidentally, if we were to take this approach, it might make sense
> to publish the API documentation for different projects at different
> URIs. Furthermore, there might be higher-level documentation of
> classes and links that are common across project-specific URIs. Would
> we put links to all of these in the `hydra:apiDocumentation` header of
> the entry point?)
> 
> 
> 
> 2. Describe links
> 
> 
> 2A. Define instances of `hydra:Link` with different supported
> operations, and use these to type links to resources. So, in our API
> documentation we could have:
> 
> ex:Note a hydra:Class .
> 
> ex:NoteCollection a hydra:Class .
> 
> ex:readNotes
>     a hydra:Link ;
>     rdfs:range ex:NoteCollection ;
>     hydra:supportedOperation [
>         a hydra:Operation ;
>         hydra:method "GET" ;
>         hydra:returns ex:NoteCollection ;
>     ] .
> ex:addNote
>     a hydra:Link ;
>     rdfs:range ex:NoteCollection ;
>     hydra:supportedOperation [
>         a hydra:CreateResourceOperation ;
>         hydra:method "POST" ;
>         hydra:expects ex:Note ;
>         hydra:returns ex:Note
>     ] .
> Then, the representation of the entry point for user A would be:
> 
> <>
>     ex:readNotes <project/1/notes/>, <project/2/notes/> ;
>     ex:addNote <project/1/notes/> .
> This approach is similar to 1A in that the API documentation will
> remain relatively static, and the types assigned to links in
> representations will change dynamically as permissions change.
> 
> 
> 2B. A variation on the approach above would be to define a
> project-specific link type for each role defined by the project
> (remember that each project can define its own roles and assign
> permissions to them). So the API documentation for project 1 (which
> defines the roles "member" and "guest") could be:
> 
> ex:Note a hydra:Class .
> 
> ex:NoteCollection a hydra:Class .
> 
> ex-project1:notesForMember
>     a hydra:Link ;
>     rdfs:range ex:NoteCollection ;
>     hydra:supportedOperation [
>         a hydra:Operation ; hydra:method "GET" ; hydra:returns
>         ex:NoteCollection ; ], [ a hydra:CreateResourceOperation ;
>         hydra:method "POST" ; hydra:expects ex:Note ; hydra:returns
>         ex:Note
>     ] .
> ex-project1:notesForGuest
>     a hydra:Link ;
>     rdfs:range ex:NoteCollection ;
>     hydra:supportedOperation [
>         a hydra:Operation ;
>         hydra:method "GET" ;
>         hydra:returns ex:NoteCollection ;
>     ] .
> And for project 2 (which defines only the role "contributor" and has a
> generic link for anyone who has not been assigned a role):
> 
> ex:Note a hydra:Class .
> 
> ex:NoteCollection a hydra:Class .
> 
> ex-project2:notesForContributor
>     a hydra:Link ;
>     rdfs:range ex:NoteCollection ;
>     hydra:supportedOperation [
>         a hydra:Operation ; hydra:method "GET" ; hydra:returns
>         ex:NoteCollection ; ], [ a hydra:CreateResourceOperation ;
>         hydra:method "POST" ; hydra:expects ex:Note ; hydra:returns
>         ex:Note
>     ] .
> ex-project2:notes
>     a hydra:Link ;
>     rdfs:range ex:NoteCollection ;
>     hydra:supportedOperation [
>         a hydra:Operation ;
>         hydra:method "GET" ;
>         hydra:returns ex:NoteCollection ;
>     ] .
> And the entry point for user A who is a "member" of project 1 but has
> no role in project 2:
> 
> <>
>     ex-project1:notesForMember <project/1/notes/> ;
>     ex-project2:notes <project/1/notes/> .
> Taking this approach, as in 2A, the API documentation will remain
> relatively static (except when role definitions change), and the types
> assigned to links in representations will change dynamically. But
> rather than having multiple links supporting different operations,
> representations could have a single role-based link.
> 
> 
> 2C. Define different instances of `hydra:Link` for different projects,
> and use these to type links to resources. This would, as in 1B,
> involve defining different "sub-APIs" per project, and varying the
> documentation of these based on permissions. So, the documentation
> that user A would see for the API to project 1 would include:
> 
> ex:Note a hydra:Class .
> 
> ex:NoteCollection a hydra:Class .
> 
> ex-project1:notes
>     a hydra:Link ;
>     rdfs:range ex:NoteCollection ;
>     hydra:supportedOperation [
>         a hydra:Operation ; hydra:method "GET" ; hydra:returns
>         ex:NoteCollection ], [ a hydra:CreateResourceOperation ;
>         hydra:method "POST" ; hydra:expects ex:Note ; hydra:returns
>         ex:Note
>     ] .
> The documentation that User A would see for the API to project 2 would include:
> 
> ex:NoteCollection a hydra:Class .
> 
> ex-project2:notes
>     a hydra:Link ;
>     rdfs:range ex:NoteCollection ;
>     hydra:supportedOperation [
>         a hydra:Operation ;
>         hydra:method "GET" ;
>         hydra:returns ex:NoteCollection
>     ] .
> And the entry point representation for user A:
> 
> <>
>     ex-project1:notes <project/1/notes/> ;
>     ex-project2:notes <project/2/notes/> .
> This approach is similar to 1B in that the API documentation for
> individual projects would change dynamically, while the representation
> of the entry point would be relatively static.
> 
> 
> 
> 3. Describe resources directly in representations using `hydra:operation`
> 
> This approach foregoes describing supported operations in the API
> documentation entirely, in favor of attaching operations directly to
> resources. The representation of the entry point would thus look
> something like:
> 
> ex:Note a hydra:Class .
> 
> ex:NoteCollection a hydra:Class .
> 
> ex:notes a hydra:Link .
> 
> <>
>     ex:notes <project/1/notes/> ;
>     ex:notes <project/2/notes/> .
> <project/1/notes>
>     hydra:operation [
>         a hydra:Operation ; hydra:method "GET" ; hydra:returns
>         ex:NoteCollection ], [ a hydra:CreateResourceOperation ;
>         hydra:method "POST" ; hydra:expects ex:Note ; hydra:returns
>         ex:Note
>     ] .
> <project/2/notes>
>     hydra:operation [
>         a hydra:Operation ;
>         hydra:method "GET" ;
>         hydra:returns ex:NoteCollection
>     ] .

Received on Sunday, 1 November 2015 15:26:21 UTC