``pgwui.validate_hmac`` setting to ``False``. Having validation off
in production is not recommended.
+Writing Plugable Components
+---------------------------
+
+Your setup.py must include a ``pgwui.components`` entry point.\ [#f1]_
+The value assigned to the given module must be the name of the PGWUI
+component which the module impliments. There must also be a
+``pgwui.check_settings`` entrypoint conforming to the following::
+
+ def check_settings(component_config):
+
+ component_config is a dict containing the configuration of the
+ component.
+
+ The components configuation settings should be checked,
+ particularly for required configuration keys and unknown
+ configuration keys.
+
+ Return a list of the errors found. Preferably, an error is a child
+ of UnknownSettingKeyError but it may be anything with a string
+ representation.
+
+In the case of the ``pgwui_upload`` module, both the module name and
+the component name are "pgwui_upload". The check_settings module name
+is ``check_settings`` and the function which does the check has the
+same name. So the entry point assignment looks like::
+
+ # Setup an entry point to support PGWUI autoconfigure discovery.
+ entry_points={'pgwui.components': '.pgwui_upload = pgwui_upload',
+ 'pgwui.check_settings':
+ '.pgwui_upload = check_settings:check_settings'}
+
+
+Your module's ``__init__.py`` must setup the component's default
+configuration::
+
+ '''Provide a way to configure PGWUI.
+ '''
+ PGWUI_COMPONENT = 'pgwui_componentname'
+ DEFAULT_COMPONENTNAME_ROUTE = '/componentname'
+ DEFAULT_COMPONENTNAME_MENU_LABEL = \
+ 'componentname -- Example PGWUI Component Label'
+
+
+ def init_menu(config):
+ '''Add default menu information into settings when they are not present
+ '''
+ settings = config.get_settings()
+ settings.setdefault('pgwui', dict())
+ settings['pgwui'].setdefault(PGWUI_COMPONENT, dict())
+ settings['pgwui'][PGWUI_COMPONENT].setdefault(
+ 'menu_label', DEFAULT_COMPONENTNAME_MENU_LABEL)
+
+
+ def includeme(config):
+ '''Pyramid configuration for PGWUI_Componentname
+ '''
+ init_menu(config)
+ config.add_route(PGWUI_COMPONENT, DEFAULT_COMPONENTNAME_ROUTE)
+ config.scan()
+
Complete Documentation
----------------------
.. _Pyramid: https://trypyramid.com/
.. _WSGI: https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface
.. _pip: https://pip.pypa.io/en/stable/
+
+
+.. rubric:: Footnotes
+
+.. [#f1] https://setuptools.readthedocs.io/en/latest/setuptools.html#dynamic-discovery-of-services-and-plugins
# Functions
-def abort_on_bad_setting(errors, key, component_keys):
+def abort_on_bad_setting(
+ errors, component_keys, component_checkers, key, settings):
'''Abort on a bad pgwui setting
'''
if key[:6] == 'pgwui.':
- if (key[6:] not in SETTINGS
- and key not in component_keys):
- errors.append(exceptions.UnknownSettingKeyError(key))
+ if key in component_keys:
+ component = component_keys[key]
+ if component in component_checkers:
+ errors.extend(
+ component_checkers[component](settings.get(key, {})))
+ else:
+ if key[6:] not in SETTINGS:
+ errors.append(exceptions.UnknownSettingKeyError(key))
def require_setting(errors, setting, settings):
settings[key] = dict(parse_assignments(settings[key]))
+def map_keys_to_components(components):
+ return dict([(plugin.component_to_key(component), component)
+ for component in components])
+
+
def validate_settings(errors, settings, components):
'''Be sure all settings validate
'''
- component_keys = [plugin.component_to_key(component)
- for component in components]
+ component_keys = map_keys_to_components(components)
+ component_checkers = plugin.find_pgwui_check_settings()
for key in settings.keys():
parse_component_settings(component_keys, key, settings)
- abort_on_bad_setting(errors, component_keys, key)
+ abort_on_bad_setting(
+ errors, component_keys, component_checkers, key, settings)
validate_setting_values(errors, settings)
validate_hmac(errors, settings)
--- /dev/null
+# Copyright (C) 2020 The Meme Factory, Inc. http://www.karlpinc.com/
+
+# This file is part of PGWUI_Server.
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU Affero General Public License
+# as published by the Free Software Foundation, either version 3 of
+# the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public
+# License along with this program. If not, see
+# <http://www.gnu.org/licenses/>.
+#
+
+# Karl O. Pinc <kop@karlpinc.com>
+
+'''Helper routines for checking a PGWUI component's settings
+'''
+
+from ast import literal_eval
+
+from . import exceptions
+
+
+def require_settings(component, required_settings, conf):
+ errors = []
+ for setting in required_settings:
+ if setting not in conf:
+ errors.append(exceptions.MissingSettingError(
+ '{}.{}'.format(component, setting)))
+ return errors
+
+
+def unknown_settings(component, settings, conf):
+ errors = []
+ for setting in conf:
+ if setting not in settings:
+ errors.append(exceptions.UnknownSettingKeyError(
+ '{}.{}'.format(component, setting)))
+ return errors
+
+
+def boolean_settings(component, booleans, conf):
+ errors = []
+ for setting in booleans:
+ if setting in conf:
+ try:
+ val = literal_eval(conf[setting])
+ except ValueError:
+ val = None
+ if (val is not True
+ and val is not False):
+ errors.append(exceptions.NotBooleanSettingError(
+ '{}.{}'.format(component, setting), conf[setting]))
+ return errors
# Karl O. Pinc <kop@karlpinc.com>
+import copy
import logging
import pytest
import sys
+import unittest.mock
import pyramid.testing
mock_find_pgwui_components = testing.make_mock_fixture(
pgwui_common.plugin, 'find_pgwui_components')
+mock_find_pgwui_check_settings = testing.make_mock_fixture(
+ pgwui_common.plugin, 'find_pgwui_check_settings')
mock_component_to_key = testing.make_mock_fixture(
pgwui_common.plugin, 'component_to_key')
def test_abort_on_bad_setting_unknown():
'''Nothing bad happens when there's a non-pgwui setting'''
errors = []
- pgwui_server_init.abort_on_bad_setting(errors, 'foo', [])
+ pgwui_server_init.abort_on_bad_setting(errors, {}, {}, 'foo', {})
assert errors == []
def test_abort_on_bad_setting_bad():
'''Delivers an error on a bad pgwui setting'''
errors = []
- pgwui_server_init.abort_on_bad_setting(errors, 'pgwui.foo', [])
+ pgwui_server_init.abort_on_bad_setting(
+ errors, {}, {}, 'pgwui.foo', {})
assert errors
assert isinstance(errors[0], ex.UnknownSettingKeyError)
def test_abort_on_bad_setting_good():
'''Does nothing when a known pgwui setting is supplied'''
errors = []
- pgwui_server_init.abort_on_bad_setting(errors, 'pgwui.pg_host', [])
+ pgwui_server_init.abort_on_bad_setting(
+ errors, {}, {}, 'pgwui.pg_host', {})
assert errors == []
-def test_abort_on_bad_setting_plugin():
- '''Does nothing when a known plugin has a setting'''
+def test_abort_on_bad_setting_plugin_no_config():
+ '''Does nothing when a known plugin has a setting and there is no
+ config for the plugin'''
errors = []
pgwui_server_init.abort_on_bad_setting(
- errors, 'pgwui.pgwui_upload', ['pgwui.pgwui_upload'])
+ errors, {'pgwui.pgwui_upload': None}, {}, 'pgwui.pgwui_upload', {})
assert errors == []
+def test_abort_on_bad_setting_plugin_config():
+ '''Calls the component checker with the component's config
+ and appends the result of the call to the errors.
+ '''
+ orig_errors = ['old', 'errors']
+ new_errors = ['some', 'errors']
+ component = 'test component'
+ sample_key = 'pgwui.pgwui_upload'
+ sample_config = {sample_key: 'some sample config'}
+
+ errors = copy.deepcopy(orig_errors)
+ mock_checker = unittest.mock.Mock(side_effect=lambda *args: new_errors)
+
+ pgwui_server_init.abort_on_bad_setting(
+ errors, {sample_key: component}, {component: mock_checker},
+ sample_key, sample_config)
+
+ mock_checker.assert_called_once
+ assert list(mock_checker.call_args[0]) == [sample_config[sample_key]]
+ assert errors == orig_errors + new_errors
+
+
mock_abort_on_bad_setting = testing.make_mock_fixture(
pgwui_server_init, 'abort_on_bad_setting')
pgwui_server_init, 'parse_component_settings')
+# map_keys_to_components()
+
+def test_map_keys_to_components(mock_component_to_key):
+ '''Returns expected result
+ '''
+ components = ['a', 'b', 'c']
+ mock_component_to_key.side_effect = ['keya', 'keyb', 'keyc']
+
+ result = pgwui_server_init.map_keys_to_components(components)
+
+ assert result == {'keya': 'a', 'keyb': 'b', 'keyc': 'c'}
+
+
+mock_map_keys_to_components = testing.make_mock_fixture(
+ pgwui_server_init, 'map_keys_to_components')
+
+
# validate_settings()
-def test_validate_settings(mock_component_to_key,
+def test_validate_settings(mock_map_keys_to_components,
mock_parse_component_settings,
+ mock_find_pgwui_check_settings,
mock_abort_on_bad_setting,
mock_validate_setting_values,
mock_validate_hmac):
settings = {'key1': 'value1',
'key2': 'value2'}
components = ['pgwui_server']
+ key_map = {'pgwui.server': 'anentrypoint'}
- mock_component_to_key.side_effect = ['pgwui.pgwui_server']
+ mock_map_keys_to_components.side_effect = lambda *args: key_map
errors = []
pgwui_server_init.validate_settings(errors, settings, components)
assert mock_validate_hmac.called
assert mock_abort_on_bad_setting.call_count == len(settings)
- assert mock_abort_on_bad_setting.call_args[0][1] == \
- ['pgwui.{}'.format(components[0])]
+ assert mock_abort_on_bad_setting.call_args[0][1] == key_map
mock_validate_settings = testing.make_mock_fixture(
--- /dev/null
+# Copyright (C) 2018, 2019, 2020 The Meme Factory, Inc.
+# http://www.karlpinc.com/
+
+# This file is part of PGWUI_Server.
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU Affero General Public License
+# as published by the Free Software Foundation, either version 3 of
+# the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public
+# License along with this program. If not, see
+# <http://www.gnu.org/licenses/>.
+#
+
+# Karl O. Pinc <kop@karlpinc.com>
+
+import pgwui_server.exceptions as ex
+import pgwui_server.checkset as checkset
+
+
+# require_settings()
+
+def test_require_settings_good():
+ '''No errors when the required settings are in the config
+ '''
+ required = ['settinga', 'settingb']
+ settings = {'settinga': 'a', 'settingb': 'b'}
+
+ result = checkset.require_settings('testcomp', required, settings)
+
+ assert result == []
+
+
+def test_require_settings_bad():
+ '''Errors when the required settings are not in the config
+ '''
+ required = ['settinga', 'settingb']
+ settings = {}
+
+ result = checkset.require_settings('testcomp', required, settings)
+
+ assert len(result) == len(required)
+ for error in result:
+ assert isinstance(error, ex.MissingSettingError)
+
+
+# unknown_settings()
+
+def test_unknown_settings_good():
+ '''There are no errors when all settings are known
+ '''
+ settings = ['settinga', 'settingb']
+ conf = {'settinga': 'a', 'settingb': 'b'}
+
+ result = checkset.unknown_settings('testcomp', settings, conf)
+
+ assert result == []
+
+
+def test_unknown_settings_bad():
+ '''Errors when settings are not known
+ '''
+ conf = {'settinga': 'a', 'settingb': 'b'}
+
+ result = checkset.unknown_settings('testcomp', [], conf)
+
+ assert len(result) == len(conf)
+ for error in result:
+ assert isinstance(error, ex.UnknownSettingKeyError)
+
+
+# boolean_settings()
+
+def test_boolean_settings_good():
+ '''No errors when boolean settings are boolean
+ '''
+ conf = {'settinga': 'a', 'settingb': 'True', 'settingc': 'False'}
+ bools = ['settingc', 'settingb']
+
+ result = checkset.boolean_settings('testcomp', bools, conf)
+
+ assert result == []
+
+
+def test_boolean_settings_bad():
+ '''Errors when boolean settings are not boolean
+ '''
+ conf = {'settinga': 'a', 'settingb': 'True', 'settingc': 'c'}
+ bools = ['settinga', 'settingb']
+
+ result = checkset.boolean_settings('testcomp', bools, conf)
+
+ assert len(result) == 1
+ for error in result:
+ assert isinstance(error, ex.NotBooleanSettingError)
+
+
+def test_boolean_settings_missing():
+ '''No errors when the boolean setting is missing from the config
+ '''
+ conf = {}
+ bools = ['settinga']
+
+ result = checkset.boolean_settings('testcomp', bools, conf)
+
+ assert result == []