Skip to content

Commit a6e36f6

Browse files
authoredJan 17, 2025··
Merge pull request #11116 from IQSS/10340-forbidden
Correct /api/roles API to respond with 403 Forbidden (not 401 Unauthorized) when auth is good but no permission
2 parents d70de2c + edb18d1 commit a6e36f6

File tree

5 files changed

+75
-11
lines changed

5 files changed

+75
-11
lines changed
 

‎doc/release-notes/10340-forbidden.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
### Backward Incompatible Changes
2+
3+
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.

‎doc/sphinx-guides/source/api/changelog.rst

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ v6.6
1111
----
1212

1313
- **/api/metadatablocks** is no longer returning duplicated metadata properties and does not omit metadata properties when called.
14+
- **/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.
1415

1516
v6.5
1617
----

‎doc/sphinx-guides/source/api/native-api.rst

+40-9
Original file line numberDiff line numberDiff line change
@@ -435,13 +435,13 @@ Creates a new role under Dataverse collection ``id``. Needs a json file with the
435435
export SERVER_URL=https://demo.dataverse.org
436436
export ID=root
437437
438-
curl -H "X-Dataverse-key:$API_TOKEN" -X POST "$SERVER_URL/api/dataverses/$ID/roles" --upload-file roles.json
438+
curl -H "X-Dataverse-key:$API_TOKEN" -H "Content-type:application/json" -X POST "$SERVER_URL/api/dataverses/$ID/roles" --upload-file roles.json
439439
440440
The fully expanded example above (without environment variables) looks like this:
441441

442442
.. code-block:: bash
443443
444-
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
444+
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
445445
446446
For ``roles.json`` see :ref:`json-representation-of-a-role`
447447

@@ -4583,17 +4583,49 @@ Create Role
45834583
45844584
Roles can be created globally (:ref:`create-global-role`) or for individual Dataverse collections (:ref:`create-role-in-collection`).
45854585
4586+
.. _show-role:
4587+
45864588
Show Role
45874589
~~~~~~~~~
45884590
4589-
Shows the role with ``id``::
4591+
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.
4592+
4593+
An example using a role alias:
4594+
4595+
.. code-block:: bash
4596+
4597+
export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
4598+
export SERVER_URL=https://demo.dataverse.org
4599+
export ALIAS=sys1
4600+
4601+
curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/roles/:alias?alias=$ALIAS"
4602+
4603+
The fully expanded example above (without environment variables) looks like this:
4604+
4605+
.. code-block:: bash
4606+
4607+
curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" "https://demo.dataverse.org/api/roles/:alias?alias=sys1"
4608+
4609+
An example using a role id:
45904610
4591-
GET http://$SERVER/api/roles/$id
4611+
.. code-block:: bash
4612+
4613+
export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
4614+
export SERVER_URL=https://demo.dataverse.org
4615+
export ID=11
4616+
4617+
curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/roles/$ID"
4618+
4619+
The fully expanded example above (without environment variables) looks like this:
4620+
4621+
.. code-block:: bash
4622+
4623+
curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" "https://demo.dataverse.org/api/roles/11"
45924624
45934625
Delete Role
45944626
~~~~~~~~~~~
45954627
4596-
A curl example using an ``ID``
4628+
An example using a role id:
45974629
45984630
.. code-block:: bash
45994631
@@ -4609,22 +4641,21 @@ The fully expanded example above (without environment variables) looks like this
46094641
46104642
curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X DELETE "https://demo.dataverse.org/api/roles/24"
46114643
4612-
A curl example using a Role alias ``ALIAS``
4644+
An example using a role alias:
46134645
46144646
.. code-block:: bash
46154647
46164648
export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
46174649
export SERVER_URL=https://demo.dataverse.org
4618-
export ALIAS=roleAlias
4650+
export ALIAS=sys1
46194651
46204652
curl -H "X-Dataverse-key:$API_TOKEN" -X DELETE "$SERVER_URL/api/roles/:alias?alias=$ALIAS"
46214653
46224654
The fully expanded example above (without environment variables) looks like this:
46234655
46244656
.. code-block:: bash
46254657
4626-
curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X DELETE "https://demo.dataverse.org/api/roles/:alias?alias=roleAlias"
4627-
4658+
curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X DELETE "https://demo.dataverse.org/api/roles/:alias?alias=sys1"
46284659
46294660
Explicit Groups
46304661
---------------

‎src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java

+21-1
Original file line numberDiff line numberDiff line change
@@ -831,6 +831,18 @@ protected Response badRequest(String msg, Map<String, String> fieldErrors) {
831831
.build();
832832
}
833833

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

854866
protected Response permissionError( String message ) {
855-
return unauthorized( message );
867+
return forbidden( message );
856868
}
857869

870+
/**
871+
* In short, bad password.
872+
*
873+
* "The 401 (Unauthorized) status code indicates that the request has not
874+
* been applied because it lacks valid authentication credentials for the
875+
* target resource." --
876+
* https://datatracker.ietf.org/doc/html/rfc7235#section-3.1
877+
*/
858878
protected Response unauthorized( String message ) {
859879
return error( Status.UNAUTHORIZED, message );
860880
}

‎src/test/java/edu/harvard/iq/dataverse/api/RolesIT.java

+10-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import io.restassured.RestAssured;
55
import io.restassured.path.json.JsonPath;
66
import io.restassured.response.Response;
7+
import static jakarta.ws.rs.core.Response.Status.FORBIDDEN;
78
import java.util.logging.Logger;
89
import static org.hamcrest.CoreMatchers.equalTo;
910
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -69,7 +70,15 @@ public void testCreateDeleteRoles() {
6970
body = addBuiltinRoleResponse.getBody().asString();
7071
status = JsonPath.from(body).getString("status");
7172
assertEquals("OK", status);
72-
73+
74+
Response createNoPermsUser = UtilIT.createRandomUser();
75+
createNoPermsUser.prettyPrint();
76+
String noPermsapiToken = UtilIT.getApiTokenFromResponse(createNoPermsUser);
77+
78+
Response noPermsResponse = UtilIT.viewDataverseRole("testRole", noPermsapiToken);
79+
noPermsResponse.prettyPrint();
80+
noPermsResponse.then().assertThat().statusCode(FORBIDDEN.getStatusCode());
81+
7382
Response viewDataverseRoleResponse = UtilIT.viewDataverseRole("testRole", apiToken);
7483
viewDataverseRoleResponse.prettyPrint();
7584
body = viewDataverseRoleResponse.getBody().asString();

0 commit comments

Comments
 (0)
Please sign in to comment.