Modeling permissions with Hydra

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
    ] .

Received on Thursday, 29 October 2015 15:33:25 UTC