Re: Modeling permissions with Hydra

On 29 October 2015 at 16:32, Ryan Shaw <ryanshaw@unc.edu> 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.) For those
> readers that, like me, were unclear on the differences among these
> approaches, I've gone into pedantic detail descibing them below, but
> 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.
>
> 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
> seems relevant is cacheability. Is it better to have API documentation
> that varies infrequently, or specific resource representations that
> change infrequently? 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?
>
> Cheers,
> Ryan
>
> [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
>     ] .
>
>
Interesting approach.  In Solid [1] we divide permissions into
read,write,control and append.  Similar to UNIX permissions.

[1] https://github.com/solid/solid-spec

Received on Thursday, 29 October 2015 16:09:44 UTC