- From: Ryan Shaw <ryanshaw@unc.edu>
- Date: Thu, 29 Oct 2015 11:32:47 -0400
- To: <public-hydra@w3.org>
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