Restricting Permissions on XAPI Calls with "restrictTo" Annotations
The @XapiRequestMapping annotation provides the restrictTo attribute to allow developers to control access to particular REST API calls. The user's access level and contingent ability to call a function depends on the context of the call itself, specifically the user's access to the resource or resources identified through the parameters to the call.
As an example, the following annotation requires that a user have "Read" access to a project's data (i.e. subjects and experiments) in order to return a 200 result:
Example RestrictTo=Read setting in a project context
@XapiRequestMapping(value = "sample/{projectId}", produces = MediaType.TEXT_PLAIN_VALUE, method = RequestMethod.GET, restrictTo = Read)
public String sampleMethod(@PathVariable @Project final String projectId) {
Note: Permissions Changes in XNAT 1.7.6
The restrictTo permissions definitions have been refactored and rationalized in XNAT 1.7.6, particularly with respect to projects. In XNAT 1.7.5.x, "restrictTo = Read" on a project referred to the user's ability to read a project's metadata. This would return true for a "Protected" project, whether or not the logged-in user had any role on that project. This led to some unexpected data exposure, particularly for plugin developers that did not expect this behavior.
Defining Access Levels
There are three levels of operational access a user might have to a resource and somewhat different implications for what a particular access level means based on the type of object–project, subject, or experiment–that the user is trying to access (access to particular subjects and experiments is always qualified by the context of the project in which the user is trying to access them).
"Experiment" in the table below doesn't necessarily apply to all experiments for a project or subject, since access to experiments can be controlled by data type so that, within the context of a particular project, a user may be able to delete experiments of type A, edit experiments of type B, read experiments of type C, and have no access to experiments of type D. However, what it means to read, write, and delete is the same for all data types.
Also, note that a user's access level for resources is the same as the user's access level for the project, subject, or experiment with which the resources are associated.
Level | Project | Subject | Experiment |
---|---|---|---|
Read Example: Project Collaborator | View the contents of the project, including at least the ability to read subjects within the project, if not necessarily experiments | List and view subjects | List and view experiments |
Write Example: Project Member | Modify project settings and configurations | Create new and edit existing subjects | Create new and edit existing experiments |
Delete Example: Project Owner or Site Admin with All Data Access | Delete project | Delete subjects | Delete experiments |
Permissions related to "Protected" Projects
Protected projects don't fit neatly into the table above: users that aren't members of a group with access to the project–such as site administrators, data administrators, and collaborators, members, and owners of the project–can "read" the project in the sense that they can see that it exists, as well as a subset of the project's metadata. However they can't read any contents of the project. An analogy would be a directory on a standard Linux file system: all users can see that particular folders exist at the top level of the file system, but users without read access to a particular folder can't list the contents of the folder or see anything about it other than the fact that it's there. Because the use cases in which the difference between protected and private projects are so limited, the XAPI framework provides no specific access level or control for them. Attempting to read a protected project to which a user does not have read permissions should result in denial of access.
Annotation Use Cases: Determining Access Level In Context
Setting the restrictTo attribute to Read, Write, or Delete requires providing a context whereby the XapiAuthorization implementation can determine the object or objects to evaluate. This context is defined by developers in response to the use case they want to support, and is coded using parameters provided to the REST API call itself. This table details a matchup of use cases with annotations.
Use Case | Annotation | Required Value | Evaluation |
---|---|---|---|
To set a restriction based on access to a project itself | @Project | Project ID | Tests whether the user has the requested access level to the project indicated by the @Project-annotated parameter. The parameter value must contain a project ID. |
To set a restriction based on access to a specific subject | @Subject or @Project @Subject | Subject ID or Project ID and Subject ID/label | Tests whether the user has the requested access level to the subject indicated by the @Subject-annotated parameter:
The project context is significant. For example, a user might be able to edit a subject in project A, but only read the subject when shared into project B. Note that the same subject data object is returned regardless of whether the subject is accessed through the source project or one into which the subject is shared, so the same rules apply for data objects as for IDs and labels. |
To set a restriction based on access to a specific experiment | @Experiment or @Project @Experiment or @Project @Subject @Experiment | Experiment ID or Project ID and Experiment ID/label or Project ID and Subject ID/label and Experiment ID/label | Tests whether the user has the requested access level to the subject indicated by the @Experiment-annotated parameter:
The project context is significant. For example, a user might be able to edit an experiment in project A, but only read the experiment when shared into project B. Note that the same experiment data object is returned regardless of whether the subject is accessed through the source project or one into which the subject is shared, so the same rules apply for data objects as for IDs and labels. |
Developer Note
To help clarify what users can perform Create / Edit / Delete operations on which data while developing, you can query Postgres and parse the result.
Practical Usages
Here is a mapping of how these usages are applied in combination with Edit and Delete permissions, and how XNAT interprets these combinations in various contexts.
Action | Annotations | Further Context | Outcome |
---|---|---|---|
Delete | @Project + @Experiment | Experiment identifies an experiment that was created in the project identified by Project. | The experiment will be removed from the XNAT database. If the experiment was shared into any other project, that experiment is no longer visible in those projects. |
Delete | @Experiment | See above | |
Delete | @Subject + @Experiment | See above | |
Delete | @Project + @Experiment | In this context, the project identified by Project is not the original project for the experiment identified by Experiment. That is, the experiment has been shared into this project and was not originally created in this project. | The experiment will be removed from the project. The experiment will still exist in the original project and in any other projects in which it was shared. |
Edit | @Project + @Experiment | Experiment identifies an experiment that was created in the project identified by Project. | Experiment will be edited |
Edit | @Experiment | See above | |
Edit | @Subject + @Experiment | See above | |
Edit | @Project + @Experiment | In this context, the project identified by Project is not the original project for the experiment identified by Experiment. That is, the experiment has been shared into this project and was not originally created in this project. | |
Edit | @Project + @Subject | Source | Easy |
Edit | @Project + @Subject | shared | wonky |
Examples
The following examples illustrate uses of the restrictTo attribute in conjunction with the @Project, @Subject, and @Experiment annotations. In each case, the result of the authorization operation will be one of three outcomes:
- An exception if the method does not provide enough information to determine the security context
- HTTP 403 if the user doesn't have sufficient access to the indicated resource
- The result of the operation implementation
Note that this doesn't mean that the user might get, e.g., a 404 when the REST endpoint isn't exposed to unauthenticated users or similar circumstances. However, once the higher-level security filters have allowed the call to proceed to the actual endpoint, the authorization implementation should only produce one of the three outcomes above.
Allow access if the user can read the project (including at least subjects):
Example code for doing something like this
JAVA@XapiRequestMapping(value = "sample/{projectId}", produces = MediaType.TEXT_PLAIN_VALUE, method = RequestMethod.GET, restrictTo = Read) public String sampleMethod(@PathVariable @Project final String projectId) {
Allow access if the user can edit a project itself.
Example code for doing something like this
JAVA@XapiRequestMapping(value = "sample/{projectId}", produces = MediaType.TEXT_PLAIN_VALUE, method = RequestMethod.GET, restrictTo = Edit) public String sampleMethod(@PathVariable @Project final String projectId) {
Allow access if the user can delete a project itself.
Example code for doing something like this
JAVA@XapiRequestMapping(value = "sample/{projectId}", produces = MediaType.TEXT_PLAIN_VALUE, method = RequestMethod.GET, restrictTo = Delete) public String sampleMethod(@PathVariable @Project final String projectId) {
Allow access if the user can delete a subject in a project.
Example code for doing something like this
JAVA@XapiRequestMapping(value = "sample/{projectId}/{subjectId}", produces = MediaType.TEXT_PLAIN_VALUE, method = RequestMethod.GET, restrictTo = Delete) public String sampleMethod(@PathVariable @Project final String projectId, @PathVariable @Subject final String subjectId) {
Allow access if the user can edit an experiment in a project.
Example code for doing something like this
JAVA@XapiRequestMapping(value = "sample/{projectId}/{experimentId}", produces = MediaType.TEXT_PLAIN_VALUE, method = RequestMethod.GET, restrictTo = Edit) public String sampleMethod(@PathVariable @Project final String projectId, @PathVariable @Experiment final String experimentId) {
Allow access if the user is a site admin.
Example code for doing something like this
JAVA@XapiRequestMapping(value = "sample", produces = MediaType.TEXT_PLAIN_VALUE, method = RequestMethod.GET, restrictTo = Admin) public ResponseEntity<String> sampleMethod() {
Allow access if the user is authenticated. Unauthenticated users are blocked from calling these methods even on open XNATs.
Example code for doing something like this
JAVA@XapiRequestMapping(value = "sample", produces = MediaType.TEXT_PLAIN_VALUE, method = RequestMethod.GET, restrictTo = Authenticated) public String sampleMethod() {
Restrict user-modifying method to site admins and the users themselves (check whether the username passed in via the @Username annotated parameter belongs to the user executing the request or whether the user executing the request is an admin).
Example code for doing something like this
JAVA@XapiRequestMapping(value = "sample/{username}", produces = MediaType.TEXT_PLAIN_VALUE, method = RequestMethod.GET, restrictTo = User) public ResponseEntity<String> sampleMethod(@PathVariable("username") @Username final String username) {
Check whether user has one of the specified roles.
Example code for doing something like this
JAVA@AuthorizedRoles({"Dqr", "Administrator"}) @XapiRequestMapping(value = "sample", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.GET, restrictTo = Role) public ResponseEntity<String> sampleMethod() {
Check whether the user has enough permissions to add the specified user to the specified groups
Example code for doing something like this
JAVA@XapiRequestMapping(value = "{username}", produces = APPLICATION_JSON_VALUE, method = PUT, restrictTo = Authorizer) @AuthDelegate(UserGroupXapiAuthorization.class) public ResponseEntity<String> sampleMethod(@PathVariable("username") @Username final String username, @RestUserGroup @RequestBody final List<String> groups) {
This example uses restrictTo=Authorizer, which is used for custom permissions checks. These will often be specified in Authorization classes in plugins, but in the cases discussed here, the Authorization classes are in core XNAT itself.
Check whether the user is logged in (if the XNAT is not an open XNAT). Only exclude the guest user if the site is not open.
Example code for doing something like this
JAVA@XapiRequestMapping(value = "sample", produces = APPLICATION_JSON_VALUE, method = GET, restrictTo = Authorizer) @AuthDelegate(GuestUserAccessXapiAuthorization.class) public ResponseEntity<String> sampleMethod() {
Check whether the user can access all the site configuration preferences in a given list.
Example code for doing something like this
JAVA@XapiRequestMapping(value = "{preferences}", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.GET, restrictTo = Authorizer) @AuthDelegate(SiteConfigPreferenceXapiAuthorization.class) public ResponseEntity<String> sampleMethod(@PathVariable final List<String> preferences) {
Check whether the user can access information about the users on the site.
Example code for doing something like this
JAVA@XapiRequestMapping(value = "", produces = APPLICATION_JSON_VALUE, method = GET, restrictTo = Authorizer) @AuthDelegate(UserResourceXapiAuthorization.class) public ResponseEntity<String> sampleMethod() {