diff --git a/news/1258.bugfix b/news/1258.bugfix new file mode 100644 index 0000000000..9460f28aec --- /dev/null +++ b/news/1258.bugfix @@ -0,0 +1 @@ +Adjust restrictions of vocabularies endpoint [ksuess] diff --git a/src/plone/restapi/services/vocabularies/configure.zcml b/src/plone/restapi/services/vocabularies/configure.zcml index 14956d953b..8091472727 100644 --- a/src/plone/restapi/services/vocabularies/configure.zcml +++ b/src/plone/restapi/services/vocabularies/configure.zcml @@ -8,7 +8,7 @@ accept="application/json" factory=".get.VocabulariesGet" for="Products.CMFPlone.interfaces.IPloneSiteRoot" - permission="plone.restapi.vocabularies" + permission="zope2.View" name="@vocabularies" /> @@ -17,7 +17,7 @@ accept="application/json" factory=".get.VocabulariesGet" for="Products.CMFCore.interfaces.IContentish" - permission="plone.restapi.vocabularies" + permission="zope2.View" name="@vocabularies" /> diff --git a/src/plone/restapi/services/vocabularies/get.py b/src/plone/restapi/services/vocabularies/get.py index 3905db0878..9b67ca178e 100644 --- a/src/plone/restapi/services/vocabularies/get.py +++ b/src/plone/restapi/services/vocabularies/get.py @@ -1,3 +1,9 @@ +from AccessControl import getSecurityManager + + +from plone.app.content.browser.vocabulary import DEFAULT_PERMISSION +from plone.app.content.browser.vocabulary import PERMISSIONS + from plone.restapi.interfaces import ISerializeToJson from plone.restapi.services import Service from zope.component import ComponentLookupError @@ -24,7 +30,23 @@ def _error(self, status, type, message): self.request.response.setStatus(status) return {"error": {"type": type, "message": message}} + def _has_permission_to_access_vocabulary(self, vocabulary_name): + """Check if user is authorized to access the vocabulary. + + The endpoint using this method is supposed to have no further protection (`zope.View` permission). + A vocabulary with no further protection follows the `plone.app.vocabularies.DEFAULT_PERMISSION` (usually `zope2.View`). + For further protection the dictionary `plone.app.vocabularies.PERMISSION` is used. + It is a mapping from vocabulary name to permission. + If a vocabulary is mapped there, the permission from the map is taken. + Thus vocabularies can be protected stronger than the default. + """ + sm = getSecurityManager() + return sm.checkPermission( + PERMISSIONS.get(vocabulary_name, DEFAULT_PERMISSION), self.context + ) + def reply(self): + # return list of all vocabularies if len(self.params) == 0: return [ { @@ -36,16 +58,26 @@ def reply(self): for vocab in getUtilitiesFor(IVocabularyFactory) ] - name = self.params[0] + # return single vocabulary by name + vocabulary_name = self.params[0] + if not self._has_permission_to_access_vocabulary(vocabulary_name): + return self._error( + 403, + "Not authorized", + ( + f"You are not authorized to access " + f"the vocabulary '{vocabulary_name}'." + ), + ) + try: - factory = getUtility(IVocabularyFactory, name=name) + factory = getUtility(IVocabularyFactory, name=vocabulary_name) except ComponentLookupError: return self._error( - 404, "Not Found", f"The vocabulary '{name}' does not exist" + 404, "Not Found", f"The vocabulary '{vocabulary_name}' does not exist" ) vocabulary = factory(self.context) - vocabulary_name = self.params[0] serializer = getMultiAdapter( (vocabulary, self.request), interface=ISerializeToJson ) diff --git a/src/plone/restapi/tests/test_services_vocabularies.py b/src/plone/restapi/tests/test_services_vocabularies.py index 2353f9424c..4aa1437cae 100644 --- a/src/plone/restapi/tests/test_services_vocabularies.py +++ b/src/plone/restapi/tests/test_services_vocabularies.py @@ -3,6 +3,8 @@ from plone.app.testing import SITE_OWNER_NAME from plone.app.testing import SITE_OWNER_PASSWORD from plone.app.testing import TEST_USER_ID +from plone.app.testing import TEST_USER_NAME +from plone.app.testing import TEST_USER_PASSWORD from plone.restapi.testing import PLONE_RESTAPI_DX_FUNCTIONAL_TESTING from plone.restapi.testing import RelativeSession from zope.component import getGlobalSiteManager @@ -110,6 +112,38 @@ def test_get_vocabulary(self): }, ) + def test_get_builtin_vocabulary(self): + """Check if built-in vocabularies are protected. + + See plone.app.vocabularies.PERMISSIONS + """ + self.api_session.auth = (TEST_USER_NAME, TEST_USER_PASSWORD) + + # test editor + setRoles(self.portal, TEST_USER_ID, ["Member", "Contributor", "Editor"]) + transaction.commit() + response = self.api_session.get( + "/@vocabularies/plone.app.vocabularies.Keywords" + ) + self.assertEqual(200, response.status_code) + response = response.json() + self.assertEqual( + response, + { + "@id": self.portal_url + + "/@vocabularies/plone.app.vocabularies.Keywords", # noqa + "items": [], + "items_total": 0, + }, + ) + # test Anonymous + setRoles(self.portal, TEST_USER_ID, ["Anonymous"]) + transaction.commit() + response = self.api_session.get( + "/@vocabularies/plone.app.vocabularies.Keywords" + ) + self.assertEqual(403, response.status_code) + def test_get_vocabulary_batched(self): response = self.api_session.get( "/@vocabularies/plone.restapi.tests.test_vocabulary?b_size=1"