From 3ff3c332012647a5a5f9074a78fd00a3e356d93d Mon Sep 17 00:00:00 2001 From: ksuess Date: Sat, 30 Oct 2021 16:48:40 +0200 Subject: [PATCH 1/8] Lower restriction of vocabularies endpoint Check if user is authorized to access vocabulary default permission for endpoint for all vocabularies, built-in and others, was permission for built-in vocabularies in Classic is PERMISSIONS = { "plone.app.vocabularies.Catalog": "View", "plone.app.vocabularies.Keywords": "Modify portal content", "plone.app.vocabularies.SyndicatableFeedItems": "Modify portal content", "plone.app.vocabularies.Users": "Modify portal content", "plone.app.multilingual.RootCatalog": "View", } --- .../services/vocabularies/configure.zcml | 4 +- .../restapi/services/vocabularies/get.py | 44 +++++++++++++++++-- 2 files changed, 42 insertions(+), 6 deletions(-) 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..0979485a12 100644 --- a/src/plone/restapi/services/vocabularies/get.py +++ b/src/plone/restapi/services/vocabularies/get.py @@ -1,9 +1,16 @@ +from AccessControl import getSecurityManager +try: + from plone.app.vocabularies import DEFAULT_PERMISSION, DEFAULT_PERMISSION_SECURE, PERMISSIONS +except ImportError: + from plone.app.content.browser.vocabulary import DEFAULT_PERMISSION, DEFAULT_PERMISSION_SECURE, PERMISSIONS +from plone.app.z3cform.interfaces import IFieldPermissionChecker from plone.restapi.interfaces import ISerializeToJson from plone.restapi.services import Service from zope.component import ComponentLookupError from zope.component import getMultiAdapter from zope.component import getUtilitiesFor from zope.component import getUtility +from zope.component import queryAdapter from zope.interface import implementer from zope.publisher.interfaces import IPublishTraverse from zope.schema.interfaces import IVocabularyFactory @@ -24,7 +31,24 @@ 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 built-in vocabulary + + default permission for all vocabularies, built-in and others, was + + """ + if vocabulary_name in PERMISSIONS: + sm = getSecurityManager() + return sm.checkPermission( + PERMISSIONS.get(vocabulary_name, DEFAULT_PERMISSION), self.context + ) + return True + def reply(self): + # return list of all vocabularies if len(self.params) == 0: return [ { @@ -36,16 +60,28 @@ 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 ) From 5a66ad7ea8e241c8d69b5fa09ab3d87ad4bc8aaf Mon Sep 17 00:00:00 2001 From: ksuess Date: Sat, 30 Oct 2021 20:08:01 +0200 Subject: [PATCH 2/8] Test authorization for accessing special built-in vocabularies See plone.app.vocabularies.PERMISSIONS --- .../tests/test_services_vocabularies.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/plone/restapi/tests/test_services_vocabularies.py b/src/plone/restapi/tests/test_services_vocabularies.py index 2353f9424c..6bd8d77d6a 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" From 3a2b68be08d1b1d96cf53b6643493bf566181a85 Mon Sep 17 00:00:00 2001 From: ksuess Date: Sun, 31 Oct 2021 07:05:21 +0100 Subject: [PATCH 3/8] black, flake8, isort --- .../restapi/services/vocabularies/get.py | 19 +++++++++++-------- .../tests/test_services_vocabularies.py | 2 +- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/plone/restapi/services/vocabularies/get.py b/src/plone/restapi/services/vocabularies/get.py index 0979485a12..6e14671160 100644 --- a/src/plone/restapi/services/vocabularies/get.py +++ b/src/plone/restapi/services/vocabularies/get.py @@ -1,16 +1,21 @@ from AccessControl import getSecurityManager + + try: - from plone.app.vocabularies import DEFAULT_PERMISSION, DEFAULT_PERMISSION_SECURE, PERMISSIONS + from plone.app.vocabularies import DEFAULT_PERMISSION + from plone.app.vocabularies import PERMISSIONS except ImportError: - from plone.app.content.browser.vocabulary import DEFAULT_PERMISSION, DEFAULT_PERMISSION_SECURE, PERMISSIONS -from plone.app.z3cform.interfaces import IFieldPermissionChecker + from plone.app.content.browser.vocabulary import ( + DEFAULT_PERMISSION, + PERMISSIONS, + ) + from plone.restapi.interfaces import ISerializeToJson from plone.restapi.services import Service from zope.component import ComponentLookupError from zope.component import getMultiAdapter from zope.component import getUtilitiesFor from zope.component import getUtility -from zope.component import queryAdapter from zope.interface import implementer from zope.publisher.interfaces import IPublishTraverse from zope.schema.interfaces import IVocabularyFactory @@ -69,16 +74,14 @@ def reply(self): ( f"You are not authorized to access " f"the vocabulary '{vocabulary_name}'." - ) + ), ) try: factory = getUtility(IVocabularyFactory, name=vocabulary_name) except ComponentLookupError: return self._error( - 404, - "Not Found", - f"The vocabulary '{vocabulary_name}' does not exist" + 404, "Not Found", f"The vocabulary '{vocabulary_name}' does not exist" ) vocabulary = factory(self.context) diff --git a/src/plone/restapi/tests/test_services_vocabularies.py b/src/plone/restapi/tests/test_services_vocabularies.py index 6bd8d77d6a..4aa1437cae 100644 --- a/src/plone/restapi/tests/test_services_vocabularies.py +++ b/src/plone/restapi/tests/test_services_vocabularies.py @@ -114,7 +114,7 @@ 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) From 46479451dc92d0dc511f96d36f10f0d3862ab478 Mon Sep 17 00:00:00 2001 From: ksuess Date: Sun, 31 Oct 2021 07:09:37 +0100 Subject: [PATCH 4/8] Create 1258.bugfix --- news/1258.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/1258.bugfix 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] From bc583b124103ab52d0477fcedd65eb7bbca6df48 Mon Sep 17 00:00:00 2001 From: ksuess Date: Fri, 5 Nov 2021 09:40:50 +0100 Subject: [PATCH 5/8] Import from plone.app.vocabularies.security, instead of plone.app.vocabularies --- src/plone/restapi/services/vocabularies/get.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/plone/restapi/services/vocabularies/get.py b/src/plone/restapi/services/vocabularies/get.py index 6e14671160..3987880681 100644 --- a/src/plone/restapi/services/vocabularies/get.py +++ b/src/plone/restapi/services/vocabularies/get.py @@ -2,13 +2,11 @@ try: - from plone.app.vocabularies import DEFAULT_PERMISSION - from plone.app.vocabularies import PERMISSIONS + from plone.app.vocabularies.security import DEFAULT_PERMISSION + from plone.app.vocabularies.security import PERMISSIONS except ImportError: - from plone.app.content.browser.vocabulary import ( - DEFAULT_PERMISSION, - PERMISSIONS, - ) + 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 f4ad5a967206554a72a7054fe9f96ab5b95f755b Mon Sep 17 00:00:00 2001 From: ksuess Date: Wed, 24 Nov 2021 10:31:06 +0100 Subject: [PATCH 6/8] Check DEFAULT_PERMISSION for all vocabularies (built-in and custom) --- .../restapi/services/vocabularies/get.py | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/plone/restapi/services/vocabularies/get.py b/src/plone/restapi/services/vocabularies/get.py index 3987880681..8e09386b26 100644 --- a/src/plone/restapi/services/vocabularies/get.py +++ b/src/plone/restapi/services/vocabularies/get.py @@ -35,20 +35,19 @@ def _error(self, status, type, message): return {"error": {"type": type, "message": message}} def _has_permission_to_access_vocabulary(self, vocabulary_name): - """Check if user is authorized to access built-in vocabulary + """Check if user is authorized to access the vocabulary. - default permission for all vocabularies, built-in and others, was - + The endpoint using this method is supposed to have no further protection (`zope.2Public` 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 or weaker than the default. """ - if vocabulary_name in PERMISSIONS: - sm = getSecurityManager() - return sm.checkPermission( - PERMISSIONS.get(vocabulary_name, DEFAULT_PERMISSION), self.context - ) - return True + sm = getSecurityManager() + return sm.checkPermission( + PERMISSIONS.get(vocabulary_name, DEFAULT_PERMISSION), self.context + ) def reply(self): # return list of all vocabularies From 05a7f2877cd820fb1ba9e550f84ad719315cc2ca Mon Sep 17 00:00:00 2001 From: ksuess Date: Wed, 24 Nov 2021 12:43:37 +0100 Subject: [PATCH 7/8] Modify docstring --- src/plone/restapi/services/vocabularies/get.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plone/restapi/services/vocabularies/get.py b/src/plone/restapi/services/vocabularies/get.py index 8e09386b26..d2cd478758 100644 --- a/src/plone/restapi/services/vocabularies/get.py +++ b/src/plone/restapi/services/vocabularies/get.py @@ -37,12 +37,12 @@ def _error(self, status, type, 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.2Public` permission). + 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 or weaker than the default. + Thus vocabularies can be protected stronger than the default. """ sm = getSecurityManager() return sm.checkPermission( From 264c9ea4c888c07d835414f46a12a08b51c08d01 Mon Sep 17 00:00:00 2001 From: ksuess Date: Wed, 24 Nov 2021 13:16:08 +0100 Subject: [PATCH 8/8] Switch back to former import of PERMISSIONS for vocabulary --- src/plone/restapi/services/vocabularies/get.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/plone/restapi/services/vocabularies/get.py b/src/plone/restapi/services/vocabularies/get.py index d2cd478758..9b67ca178e 100644 --- a/src/plone/restapi/services/vocabularies/get.py +++ b/src/plone/restapi/services/vocabularies/get.py @@ -1,12 +1,8 @@ from AccessControl import getSecurityManager -try: - from plone.app.vocabularies.security import DEFAULT_PERMISSION - from plone.app.vocabularies.security import PERMISSIONS -except ImportError: - from plone.app.content.browser.vocabulary import DEFAULT_PERMISSION - from plone.app.content.browser.vocabulary import PERMISSIONS +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