From 9f380751f566048a96c04df7d9836ac169c77f08 Mon Sep 17 00:00:00 2001 From: Francesco Siddi Date: Wed, 11 Jul 2018 12:32:00 +0200 Subject: [PATCH 1/2] Support for capabilities check in any shortcode Use the @capcheck decorator on any shortcode that should support this. Currently used by iframe and youtube. --- pillar/shortcodes.py | 65 +++++++++++++++++++++++++++++----------- tests/test_shortcodes.py | 15 +++++++++- 2 files changed, 62 insertions(+), 18 deletions(-) diff --git a/pillar/shortcodes.py b/pillar/shortcodes.py index 634060ca..5a4a8c32 100644 --- a/pillar/shortcodes.py +++ b/pillar/shortcodes.py @@ -33,18 +33,57 @@ log = logging.getLogger(__name__) def shortcode(name: str): """Class decorator for shortcodes.""" - def decorator(cls): - assert hasattr(cls, '__call__'), '@shortcode should be used on callables.' - if isinstance(cls, type): - instance = cls() + def decorator(decorated): + assert hasattr(decorated, '__call__'), '@shortcode should be used on callables.' + if isinstance(decorated, type): + as_callable = decorated() else: - instance = cls - shortcodes.register(name)(instance) - return cls + as_callable = decorated + shortcodes.register(name)(as_callable) + return decorated return decorator +class capcheck: + """Decorator for shortcodes. + + On call, check for capabilities before calling the function. If the user does not + have a capability, display a message insdead of the content. + + kwargs: + - 'cap': Capability required for viewing. + - 'nocap': Optional, text shown when the user does not have this capability. + - others: Passed to the decorated shortcode. + """ + + def __init__(self, decorated): + assert hasattr(decorated, '__call__'), '@capcheck should be used on callables.' + if isinstance(decorated, type): + as_callable = decorated() + else: + as_callable = decorated + self.decorated = as_callable + + def __call__(self, + context: typing.Any, + content: str, + pargs: typing.List[str], + kwargs: typing.Dict[str, str]) -> str: + from pillar.auth import current_user + + cap = kwargs.pop('cap', '') + if cap: + nocap = kwargs.pop('nocap', '') + if not current_user.has_cap(cap): + if not nocap: + return '' + html = html_module.escape(nocap) + return f'

{html}

' + + return self.decorated(context, content, pargs, kwargs) + + @shortcode('test') class Test: def __call__(self, @@ -68,6 +107,7 @@ class Test: @shortcode('youtube') +@capcheck class YouTube: log = log.getChild('YouTube') @@ -129,6 +169,7 @@ class YouTube: @shortcode('iframe') +@capcheck def iframe(context: typing.Any, content: str, pargs: typing.List[str], @@ -140,16 +181,6 @@ def iframe(context: typing.Any, - others: Turned into attributes for the iframe element. """ import xml.etree.ElementTree as ET - from pillar.auth import current_user - - cap = kwargs.pop('cap', '') - if cap: - nocap = kwargs.pop('nocap', '') - if not current_user.has_cap(cap): - if not nocap: - return '' - html = html_module.escape(nocap) - return f'

{html}

' kwargs['class'] = f'shortcode {kwargs.get("class", "")}'.strip() element = ET.Element('iframe', kwargs) diff --git a/tests/test_shortcodes.py b/tests/test_shortcodes.py index 18422aa7..a1f0ce3b 100644 --- a/tests/test_shortcodes.py +++ b/tests/test_shortcodes.py @@ -40,7 +40,7 @@ class DemoTest(unittest.TestCase): self.assertEqual('
test
ü
é
', render('{test ü="é"}')) -class YouTubeTest(unittest.TestCase): +class YouTubeTest(AbstractPillarTest): def test_missing(self): from pillar.shortcodes import render @@ -104,6 +104,19 @@ class YouTubeTest(unittest.TestCase): render('{youtube "https://www.youtube.com/watch?v=NwVGvcIrNWA" width=5 height="3"}') ) + def test_user_no_cap(self): + from pillar.shortcodes import render + + with self.app.app_context(): + # Anonymous user, so no subscriber capability. + self.assertEqual('', render('{youtube ABCDEF cap=subscriber}')) + self.assertEqual('', render('{youtube ABCDEF cap="subscriber"}')) + self.assertEqual( + '

Aðeins áskrifendur hafa aðgang að þessu efni.

', + render('{youtube ABCDEF' + ' cap="subscriber"' + ' nocap="Aðeins áskrifendur hafa aðgang að þessu efni."}')) + class IFrameTest(AbstractPillarTest): def test_missing_cap(self): From 5fb40eb32b97024e6daeca64154ed6a8f463ebb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Fri, 13 Jul 2018 11:41:01 +0200 Subject: [PATCH 2/2] Simple unittests for Cerberus validation --- tests/test_api/test_cerberus.py | 170 ++++++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 tests/test_api/test_cerberus.py diff --git a/tests/test_api/test_cerberus.py b/tests/test_api/test_cerberus.py new file mode 100644 index 00000000..89c9210f --- /dev/null +++ b/tests/test_api/test_cerberus.py @@ -0,0 +1,170 @@ +"""Test that what we feed to Cerberus actually works. + +This'll help us upgrade to new versions of Cerberus. +""" + +import unittest +from pillar.tests import AbstractPillarTest + +from bson import ObjectId + + +class CerberusCanaryTest(unittest.TestCase): + + def _canary_test(self, validator): + groups_schema = {'name': {'type': 'string', 'required': True}} + + # On error, validate_schema() raises ValidationError + validator.validate_schema(groups_schema) + + # On error, validate() returns False + self.assertTrue(validator.validate({'name': 'je moeder'}, groups_schema)) + self.assertFalse(validator.validate({'je moeder': 'op je hoofd'}, groups_schema)) + + def test_canary(self): + import cerberus + + validator = cerberus.Validator() + self._canary_test(validator) + + def test_our_validator_simple(self): + from pillar.api import custom_field_validation + + validator = custom_field_validation.ValidateCustomFields() + self._canary_test(validator) + + +class ValidationTest(AbstractPillarTest): + def setUp(self): + super().setUp() + + from pillar.api import custom_field_validation + + self.validator = custom_field_validation.ValidateCustomFields() + self.user_id = ObjectId(8 * 'abc') + self.ensure_user_exists(self.user_id, 'Tést Üsâh') + + def assertValid(self, document, schema): + with self.app.app_context(): + is_valid = self.validator.validate(document, schema) + self.assertTrue(is_valid, f'errors: {self.validator.errors}') + + def assertInvalid(self, document, schema): + with self.app.app_context(): + is_valid = self.validator.validate(document, schema) + self.assertFalse(is_valid) + + +class ProjectValidationTest(ValidationTest): + + def test_empty(self): + from pillar.api.eve_settings import projects_schema + self.assertInvalid({}, projects_schema) + + def test_simple_project(self): + from pillar.api.eve_settings import projects_schema + + project = { + 'name': 'Té Ærhüs', + 'user': self.user_id, + 'category': 'assets', + 'is_private': False, + 'status': 'published', + } + + self.assertValid(project, projects_schema) + + def test_with_node_types(self): + from pillar.api.eve_settings import projects_schema + from pillar.api import node_types + + project = { + 'name': 'Té Ærhüs', + 'user': self.user_id, + 'category': 'assets', + 'is_private': False, + 'status': 'published', + 'node_types': [node_types.node_type_asset, + node_types.node_type_comment] + } + + self.assertValid(project, projects_schema) + + +class NodeValidationTest(ValidationTest): + def setUp(self): + super().setUp() + self.pid, self.project = self.ensure_project_exists() + + def test_empty(self): + from pillar.api.eve_settings import nodes_schema + self.assertInvalid({}, nodes_schema) + + def test_asset(self): + from pillar.api.eve_settings import nodes_schema + + file_id, _ = self.ensure_file_exists() + + node = { + 'name': '"The Harmless Prototype™"', + 'project': self.pid, + 'node_type': 'asset', + 'properties': { + 'status': 'published', + 'content_type': 'image', + 'file': file_id, + }, + 'user': self.user_id, + 'short_code': 'ABC333', + } + self.assertValid(node, nodes_schema) + + def test_asset_invalid_properties(self): + from pillar.api.eve_settings import nodes_schema + + file_id, _ = self.ensure_file_exists() + + node = { + 'name': '"The Harmless Prototype™"', + 'project': self.pid, + 'node_type': 'asset', + 'properties': { + 'status': 'invalid-status', + 'content_type': 'image', + 'file': file_id, + }, + 'user': self.user_id, + 'short_code': 'ABC333', + } + self.assertInvalid(node, nodes_schema) + + def test_comment(self): + from pillar.api.eve_settings import nodes_schema + + file_id, _ = self.ensure_file_exists() + + node = { + 'name': '"The Harmless Prototype™"', + 'project': self.pid, + 'node_type': 'asset', + 'properties': { + 'status': 'published', + 'content_type': 'image', + 'file': file_id, + }, + 'user': self.user_id, + 'short_code': 'ABC333', + } + node_id = self.create_node(node) + + comment = { + 'name': 'comment on some node', + 'project': self.pid, + 'node_type': 'comment', + 'properties': { + 'content': 'this is a comment', + 'status': 'published', + }, + 'parent': node_id, + } + self.assertValid(comment, nodes_schema)