From 57d6ed45753d92f9b0b303b07be840baf0f18ba6 Mon Sep 17 00:00:00 2001 From: "Karl O. Pinc" Date: Sat, 17 Nov 2018 03:08:08 -0600 Subject: [PATCH] Require some settings, supply defaults for others, validate setting values --- examples/development.ini | 14 ++++-- examples/pgwui.ini | 16 ++++--- src/pgwui_server/__init__.py | 54 +++++++++++++++++++++ tests/test___init__.py | 92 +++++++++++++++++++++++++++++++++++- 4 files changed, 163 insertions(+), 13 deletions(-) diff --git a/examples/development.ini b/examples/development.ini index 0af2f3a..c7b14a1 100644 --- a/examples/development.ini +++ b/examples/development.ini @@ -16,11 +16,13 @@ use = egg:PGWUI_Server # Postgres client configuration. -# (An empty string for host means use the unix socket.) +# Both pgwui.pg_host and pgwui.pg_port are optional; +# they default to the Unix PG socket and the default Postgres port. +# An empty string for host means use the unix socket. pgwui.pg_host = pgwui.pg_port = 5432 -# There are occasions when PGWUI uses a default database. +# There are occasions when PGWUI uses a default database. (optional) pgwui.default_db = template1 # What PGWUI modules to use. @@ -29,7 +31,7 @@ pyramid.includes = pgwui_upload pyramid_debugtoolbar -# Whether or not to change the db content. +# Whether or not to change the db content. (required) pgwui.dry_run = False # Routing @@ -46,7 +48,7 @@ pgwui.dry_run = False # For more information on route syntax see: # https://docs.pylonsproject.org/projects/pyramid/en/master/narr/urldispatch.html#route-pattern-syntax -# A prefix for all routes. If the prefix is "/a/b/c" then +# A prefix for all routes. (optional) If the prefix is "/a/b/c" then # all URLs will begin with something like: http://example.com/a/b/c # The default is no prefix. # pgwui.route_prefix = @@ -58,6 +60,8 @@ pgwui.dry_run = False # the component. So to access the logout page at # http://example.com/logmeout the line would be: logout = /logmeout # +# Overriding routes is optional. +# # The default for some PGWUI components are: # pgwui.routes = # logout = /logout @@ -65,7 +69,7 @@ pgwui.dry_run = False # Settings validation -# Whether or not to validate the session.secret setting. +# Whether or not to validate the session.secret setting. (optional) # session.secret must be valid to detect Cross-Site Request Forgery (CSRF) # vulnerabilties. Validation is on by default. pgwui.validate_hmac = False diff --git a/examples/pgwui.ini b/examples/pgwui.ini index 8edda38..8701067 100644 --- a/examples/pgwui.ini +++ b/examples/pgwui.ini @@ -16,11 +16,13 @@ use = egg:PGWUI_Server # Postgres client configuration. -# (An empty string for host means use the unix socket.) +# Both pgwui.pg_host and pgwui.pg_port are optional; +# they default to the Unix PG socket and the default Postgres port. +# An empty string for host means use the unix socket. pgwui.pg_host = pgwui.pg_port = 5432 -# There are occasions when PGWUI uses a default database. +# There are occasions when PGWUI uses a default database. (optional) pgwui.default_db = template1 # What PGWUI modules to use. @@ -28,7 +30,7 @@ pyramid.includes = pgwui_logout pgwui_upload -# Whether or not to change the db content. +# Whether or not to change the db content. (required) pgwui.dry_run = False # Routing @@ -45,7 +47,7 @@ pgwui.dry_run = False # For more information on route syntax see: # https://docs.pylonsproject.org/projects/pyramid/en/master/narr/urldispatch.html#route-pattern-syntax -# A prefix for all routes. If the prefix is "/a/b/c" then +# A prefix for all routes. (optional) If the prefix is "/a/b/c" then # all URLs will begin with something like: http://example.com/a/b/c # The default is no prefix. # pgwui.route_prefix = @@ -57,16 +59,16 @@ pgwui.dry_run = False # the component. So to access the logout page at # http://example.com/logmeout the line would be: logout = /logmeout # +# Overriding routes is optional. +# # The default for some PGWUI components are: # pgwui.routes = # logout = /logmeout # upload = /put-in - - # Settings validation -# Whether or not to validate the session.secret setting. +# Whether or not to validate the session.secret setting. (optional) # session.secret must be valid to detect Cross-Site Request Forgery (CSRF) # vulnerabilties. Validation is on by default. # pgwui.validate_hmac = True diff --git a/src/pgwui_server/__init__.py b/src/pgwui_server/__init__.py index eb8ce80..57cbbb6 100644 --- a/src/pgwui_server/__init__.py +++ b/src/pgwui_server/__init__.py @@ -54,6 +54,18 @@ class UnknownSettingKeyError(Error): super().__init__('Unknown PGWUI setting: {}'.format(key)) +class MissingSettingError(Error): + def __init__(self, key): + super().__init__('Missing PGWUI setting: {}'.format(key)) + + +class NotBooleanSettingError(Error): + def __init__(self, key, value): + super().__init__( + 'The "{}" PGWUI setting must be "True" or "False"' + .format(key)) + + class BadHMACError(Error): pass @@ -80,6 +92,46 @@ def abort_on_bad_setting(key): raise UnknownSettingKeyError(key) +def require_setting(setting, settings): + if setting not in settings: + raise MissingSettingError(setting) + + +def boolean_setting(setting, settings): + if setting in settings: + val = literal_eval(settings[setting]) + if (val is not True + and val is not False): + raise NotBooleanSettingError(setting, settings[setting]) + + +def validate_setting_values(settings): + '''Check each settings value for validity + ''' + # pg_host can be missing, it defaults to the Unix socket (in psycopg2) + + # pg_port can be missing, it defaults to 5432 (in psycopg2) + + # default_db can be missing, then the user sees no default + + # dry_run + require_setting('pgwui.dry_run', settings) + boolean_setting('pgwui.dry_run', settings) + + # route_prefix can be missing, defaults to no route prefix which is fine. + + # routes can be missing, sensible defaults are built-in. + + # validate_hmac + boolean_setting('pgwui.validate_hmac', settings) + + +def supply_default_settings(settings): + '''Supply sensible defaults for omitted settings. + ''' + settings['pgwui.default_db'] = '' + + def do_validate_hmac(settings): '''True unless the user has specificly rejected hmac validation ''' @@ -104,6 +156,8 @@ def validate_settings(settings): ''' for key in settings.keys(): abort_on_bad_setting(key) + validate_setting_values(settings) + supply_default_settings(settings) validate_hmac(settings) diff --git a/tests/test___init__.py b/tests/test___init__.py index 09aa885..469989d 100644 --- a/tests/test___init__.py +++ b/tests/test___init__.py @@ -27,6 +27,7 @@ import pgwui_server.__init__ as pgwui_server_init TEST_SETTINGS = { 'pgwui.validate_hmac': 'False', + 'pgwui.dry_run': 'False', } @@ -76,6 +77,78 @@ def test_abort_on_bad_setting_good(): pgwui_server_init.abort_on_bad_setting('pgwui.pg_host') +# require_setting() + +def test_require_setting_missing(): + '''Raise an exception when a required setting is missing''' + with pytest.raises(pgwui_server_init.MissingSettingError): + pgwui_server_init.require_setting('key', {}) + + +def test_require_setting_present(): + '''Does nothing when a required setting is present''' + pgwui_server_init.require_setting('key', + {'key': 'value'}) + + +# boolean_setting() + +def test_boolean_setting_missing(): + '''Does nothing when the setting is not in the settings''' + pgwui_server_init.boolean_setting('key', {}) + + +def test_boolean_setting_true(): + '''Does nothing when the setting is "True"''' + pgwui_server_init.boolean_setting('key', + {'key': 'True'}) + + +def test_boolean_setting_false(): + '''Does nothing when the setting is "False"''' + pgwui_server_init.boolean_setting('key', + {'key': 'False'}) + + +def test_boolean_setting_notboolean(): + '''Raise an exception when the setting does not evaluate to a boolean''' + with pytest.raises(pgwui_server_init.NotBooleanSettingError): + pgwui_server_init.boolean_setting('key', + {'key': '0'}) + + +# validate_setting_values() + +def test_validate_setting_values(monkeypatch): + '''Calls require_setting() and boolean_setting()''' + require_setting_called = False + boolean_setting_called = False + + def mock_require_setting(*args): + nonlocal require_setting_called + require_setting_called = True + + def mock_boolean_setting(*args): + nonlocal boolean_setting_called + boolean_setting_called = True + + monkeypatch.setattr(pgwui_server_init, 'require_setting', + mock_require_setting) + monkeypatch.setattr(pgwui_server_init, 'boolean_setting', + mock_boolean_setting) + + pgwui_server_init.validate_setting_values({}) + + assert require_setting_called + assert boolean_setting_called + + +# supply_default_settings() +def test_supply_default_settings(): + '''Something is changed in the supplied settings''' + assert {} != pgwui_server_init.supply_default_settings({}) + + # do_validate_hmac() def test_do_validate_hmac_none(): @@ -143,6 +216,10 @@ def test_validate_settings(monkeypatch): monkeypatch.setattr(pgwui_server_init, 'abort_on_bad_setting', mock_abort_on_bad_setting) + monkeypatch.setattr(pgwui_server_init, 'validate_setting_values', + lambda *args: None) + monkeypatch.setattr(pgwui_server_init, 'supply_default_settings', + lambda arg: arg) monkeypatch.setattr(pgwui_server_init, 'validate_hmac', lambda *args: None) settings = {'key1': 'value1', @@ -232,7 +309,20 @@ def test_main_integrated(): # Functional tests + def test_unknownsettingkeyerror(): '''Takes an argument''' assert isinstance(pgwui_server_init.UnknownSettingKeyError('key'), - Exception) + pgwui_server_init.Error) + + +def test_missingsettingerror(): + '''Takes an argument''' + assert isinstance(pgwui_server_init.MissingSettingError('key'), + pgwui_server_init.Error) + + +def test_notbooleansettingerror(): + '''Takes two arguments''' + assert isinstance(pgwui_server_init.NotBooleanSettingError('key', 'val'), + pgwui_server_init.Error) -- 2.34.1