Skip to content

Commit

Permalink
Merge pull request #11116 from IQSS/10340-forbidden
Browse files Browse the repository at this point in the history
Correct /api/roles API to respond with 403 Forbidden (not 401 Unauthorized) when auth is good but no permission
  • Loading branch information
ofahimIQSS authored Jan 17, 2025
2 parents d70de2c + edb18d1 commit a6e36f6
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 11 deletions.
3 changes: 3 additions & 0 deletions doc/release-notes/10340-forbidden.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Backward Incompatible Changes

The [Show Role](https://dataverse-guide--11116.org.readthedocs.build/en/11116/api/native-api.html#show-role) API endpoint was returning 401 Unauthorized when a permission check failed. This has been corrected to return 403 Forbidden instead. That is, the API token is known to be good (401 otherwise) but the user lacks permission (403 is now sent). See also the [API Changelog](https://dataverse-guide--11116.org.readthedocs.build/en/11116/api/changelog.html), #10340, and #11116.
1 change: 1 addition & 0 deletions doc/sphinx-guides/source/api/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ v6.6
----

- **/api/metadatablocks** is no longer returning duplicated metadata properties and does not omit metadata properties when called.
- **/api/roles**: :ref:`show-role` now properly returns 403 Forbidden instead of 401 Unauthorized when you pass a working API token that doesn't have the right permission.

v6.5
----
Expand Down
49 changes: 40 additions & 9 deletions doc/sphinx-guides/source/api/native-api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -435,13 +435,13 @@ Creates a new role under Dataverse collection ``id``. Needs a json file with the
export SERVER_URL=https://demo.dataverse.org
export ID=root
curl -H "X-Dataverse-key:$API_TOKEN" -X POST "$SERVER_URL/api/dataverses/$ID/roles" --upload-file roles.json
curl -H "X-Dataverse-key:$API_TOKEN" -H "Content-type:application/json" -X POST "$SERVER_URL/api/dataverses/$ID/roles" --upload-file roles.json
The fully expanded example above (without environment variables) looks like this:

.. code-block:: bash
curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X POST -H "Content-type:application/json" "https://demo.dataverse.org/api/dataverses/root/roles" --upload-file roles.json
curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -H "Content-type:application/json" -X POST "https://demo.dataverse.org/api/dataverses/root/roles" --upload-file roles.json
For ``roles.json`` see :ref:`json-representation-of-a-role`

Expand Down Expand Up @@ -4583,17 +4583,49 @@ Create Role
Roles can be created globally (:ref:`create-global-role`) or for individual Dataverse collections (:ref:`create-role-in-collection`).
.. _show-role:
Show Role
~~~~~~~~~
Shows the role with ``id``::
You must have ``ManageDataversePermissions`` to be able to show a role that was created using :ref:`create-role-in-collection`. Global roles (:ref:`create-global-role`) can only be shown with a superuser API token.
An example using a role alias:
.. code-block:: bash
export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
export SERVER_URL=https://demo.dataverse.org
export ALIAS=sys1
curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/roles/:alias?alias=$ALIAS"
The fully expanded example above (without environment variables) looks like this:
.. code-block:: bash
curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" "https://demo.dataverse.org/api/roles/:alias?alias=sys1"
An example using a role id:
GET http://$SERVER/api/roles/$id
.. code-block:: bash
export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
export SERVER_URL=https://demo.dataverse.org
export ID=11
curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/roles/$ID"
The fully expanded example above (without environment variables) looks like this:
.. code-block:: bash
curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" "https://demo.dataverse.org/api/roles/11"
Delete Role
~~~~~~~~~~~
A curl example using an ``ID``
An example using a role id:
.. code-block:: bash
Expand All @@ -4609,22 +4641,21 @@ The fully expanded example above (without environment variables) looks like this
curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X DELETE "https://demo.dataverse.org/api/roles/24"
A curl example using a Role alias ``ALIAS``
An example using a role alias:
.. code-block:: bash
export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
export SERVER_URL=https://demo.dataverse.org
export ALIAS=roleAlias
export ALIAS=sys1
curl -H "X-Dataverse-key:$API_TOKEN" -X DELETE "$SERVER_URL/api/roles/:alias?alias=$ALIAS"
The fully expanded example above (without environment variables) looks like this:
.. code-block:: bash
curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X DELETE "https://demo.dataverse.org/api/roles/:alias?alias=roleAlias"
curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X DELETE "https://demo.dataverse.org/api/roles/:alias?alias=sys1"
Explicit Groups
---------------
Expand Down
22 changes: 21 additions & 1 deletion src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,18 @@ protected Response badRequest(String msg, Map<String, String> fieldErrors) {
.build();
}

/**
* In short, your password is fine but you don't have permission.
*
* "The 403 (Forbidden) status code indicates that the server understood the
* request but refuses to authorize it. A server that wishes to make public
* why the request has been forbidden can describe that reason in the
* response payload (if any).
*
* If authentication credentials were provided in the request, the server
* considers them insufficient to grant access." --
* https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.3
*/
protected Response forbidden( String msg ) {
return error( Status.FORBIDDEN, msg );
}
Expand All @@ -852,9 +864,17 @@ protected Response permissionError( PermissionException pe ) {
}

protected Response permissionError( String message ) {
return unauthorized( message );
return forbidden( message );
}

/**
* In short, bad password.
*
* "The 401 (Unauthorized) status code indicates that the request has not
* been applied because it lacks valid authentication credentials for the
* target resource." --
* https://datatracker.ietf.org/doc/html/rfc7235#section-3.1
*/
protected Response unauthorized( String message ) {
return error( Status.UNAUTHORIZED, message );
}
Expand Down
11 changes: 10 additions & 1 deletion src/test/java/edu/harvard/iq/dataverse/api/RolesIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import io.restassured.RestAssured;
import io.restassured.path.json.JsonPath;
import io.restassured.response.Response;
import static jakarta.ws.rs.core.Response.Status.FORBIDDEN;
import java.util.logging.Logger;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.jupiter.api.Assertions.assertEquals;
Expand Down Expand Up @@ -69,7 +70,15 @@ public void testCreateDeleteRoles() {
body = addBuiltinRoleResponse.getBody().asString();
status = JsonPath.from(body).getString("status");
assertEquals("OK", status);


Response createNoPermsUser = UtilIT.createRandomUser();
createNoPermsUser.prettyPrint();
String noPermsapiToken = UtilIT.getApiTokenFromResponse(createNoPermsUser);

Response noPermsResponse = UtilIT.viewDataverseRole("testRole", noPermsapiToken);
noPermsResponse.prettyPrint();
noPermsResponse.then().assertThat().statusCode(FORBIDDEN.getStatusCode());

Response viewDataverseRoleResponse = UtilIT.viewDataverseRole("testRole", apiToken);
viewDataverseRoleResponse.prettyPrint();
body = viewDataverseRoleResponse.getBody().asString();
Expand Down

0 comments on commit a6e36f6

Please sign in to comment.