Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
*.egg-info
*.pyc
.cache
.coverage
/build
/dist
/htmlcov
2 changes: 0 additions & 2 deletions requirements.txt

This file was deleted.

2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def run(self):
'pyyaml>=3.11',
],
extras_require = {
'test': ['codacy-coverage', 'mock', 'pytest', 'pytest-cov', 'python-coveralls', 'stormpath'],
'test': ['codacy-coverage', 'mock', 'pytest', 'pytest-cov', 'pytest-env', 'pytest-xdist', 'python-coveralls', 'stormpath'],
},
packages = find_packages(exclude=['*.tests', '*.tests.*', 'tests.*', 'tests']),
classifiers = [
Expand Down
8 changes: 8 additions & 0 deletions stormpath_config/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Custom errors."""


class ConfigurationError(Exception):
"""
This exception is raised if a user has stormpath configuration.
"""
pass
2 changes: 2 additions & 0 deletions stormpath_config/loader.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@


"""Configuration Loader."""


Expand Down
2 changes: 2 additions & 0 deletions stormpath_config/strategies/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@
from .load_file_config import LoadFileConfigStrategy
from .load_file_path import LoadFilePathStrategy
from .validate_client_config import ValidateClientConfigStrategy
from .move_apikey_to_client import MoveAPIKeyToClientAPIKeyStrategy
from .move_settings_to_config import MoveSettingsToConfigStrategy
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from datetime import timedelta

from stormpath_config.errors import ConfigurationError
from .enrich_client_from_remote_config import _resolve_application_by_name
from ..helpers import _extend_dict, to_camel_case


Expand Down Expand Up @@ -162,6 +164,99 @@ class EnrichIntegrationFromRemoteConfigStrategy(object):
def __init__(self, client_factory):
self.client_factory = client_factory

def social_enabled_and_empty(self, config):
if not config or not isinstance(config, dict):
return False
return config.get('enabled') and not all([
config.get('clientId'),
config.get('clientSecret')
])

def validate(self, config):
"""
Ensure the user-specified settings are valid.
This will raise a ConfigurationError if anything mandatory is not
specified.

:param dict config: The Flask app config.
"""
client = self.client_factory(config)

# Check if application information is present.
application = config.get('application')
if not application:
raise ConfigurationError('Application cannot be empty.')

href = application.get('href')
name = application.get('name')

if href:
if '/applications/' not in href:
raise ConfigurationError(
'Application HREF "%s" is not a valid Stormpath ' % href +
'Application HREF.'
)
elif name:
href = _resolve_application_by_name(client, config, name)
else:
raise ConfigurationError(
'You must specify application name or href.')
application = client.applications.get(href)


# Check if google social information is present.
google_config = config['web']['social'].get('google')

if not google_config or self.social_enabled_and_empty(google_config):
raise ConfigurationError(
'You must define your Google app settings.')

# Check if facebook social information is present.
facebook_config = config['web']['social'].get('facebook')

if not facebook_config or self.social_enabled_and_empty(facebook_config):
raise ConfigurationError(
'You must define your Facebook app settings.')

# Check if default account store is present.
if (
config['web']['register']['enabled'] and
not application.default_account_store_mapping):
raise ConfigurationError(
"No default account store is mapped to the specified "
"application. A default account store is required for "
"registration.")

# Ensure that autologin and verify email cannot be active at the same
# time.
if all([config['web']['register']['autoLogin'],
config['web']['verifyEmail']['enabled']]):
raise ConfigurationError(
"Invalid configuration: stormpath.web.register.autoLogin "
"is true, but the default account store of the "
"specified application has the email verification "
"workflow enabled. Auto login is only possible if email "
"verification is disabled. "
"Please disable this workflow on this application's default "
"account store.")

# Check if cookie information is present.
cookie = config.get('cookie')

# Check cookie settings.
if not cookie or not isinstance(cookie, dict):
raise ConfigurationError('Cookie settings cannot be empty.')

# Check cookie domain.
if cookie.get('domain') and not isinstance(
config['cookie']['domain'], str):
raise ConfigurationError('Cookie domain must be a string.')

# Check cookie duration.
if cookie.get('duration') and not isinstance(
config['cookie']['duration'], timedelta):
raise ConfigurationError('Cookie duration must be a string.')

def process(self, config):
if config.get('skipRemoteConfig'):
return config
Expand All @@ -179,4 +274,5 @@ def process(self, config):
if policy_config:
_extend_dict(config, policy_config)

self.validate(config)
return config
21 changes: 21 additions & 0 deletions stormpath_config/strategies/move_apikey_to_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
class MoveAPIKeyToClientAPIKeyStrategy(object):
""" Represents a strategy that checks if our config has an outer key named
`apiKey'. If it does, we move it to client.apiKey. """
def process(self, config=None):
if config is None:
config = {}

apiKey = config.get('apiKey', {})
if apiKey:
api_key_id = apiKey.get('id')
api_key_secret = apiKey.get('secret')
if not (api_key_id and api_key_secret):
raise Exception('Unable to load apiKey id and secret.')

config.setdefault('client', {})
config['client'].setdefault('apiKey', {})
config['client']['apiKey']['id'] = api_key_id
config['client']['apiKey']['secret'] = api_key_secret

config.pop('apiKey', {})
return config
75 changes: 75 additions & 0 deletions stormpath_config/strategies/move_settings_to_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from ..helpers import _extend_dict


class MoveSettingsToConfigStrategy(object):
"""
Checks the outer config and retrieves values whose keys start with
'STORMPATH' prefix, and stores them in the configuration object properly.
"""
STORMPATH_PREFIX = 'STORMPATH'
KEY_DELIMITER = '*'
MAPPINGS = {
'APPLICATION': 'application',
'API_KEY_ID': 'client*apiKey*id',
'API_KEY_SECRET': 'client*apiKey*secret',
'API_KEY_FILE': 'client*apiKey*file',
'ENABLE_FACEBOOK': 'web*social*facebook*enabled',
'ENABLE_GOOGLE': 'web*social*google*enabled',
'FACEBOOK_LOGIN_URL': 'web*social*facebook*login_url',
'GOOGLE_LOGIN_URL': 'web*social*google*login_url',
'CACHE': 'cache',
'BASE_TEMPLATE': 'base_template',
'COOKIE_DOMAIN': 'cookie*domain',
'COOKIE_DURATION': 'cookie*duration',
}

def __init__(self, config={}):
self.config = config

def set_key(self, config, key, value):
"""
We use this method to properly map values into stormpath config object.
Some values are nested, in which case we create sub-dictionaries if
needed.
"""
subkeys = key.split(self.KEY_DELIMITER)
if len(subkeys) > 1:
attr = subkeys.pop(-1)
subdict = config
for key in subkeys:
subdict.setdefault(key, {})
subdict = subdict[key]
subdict[attr] = value
else:
config[key] = value

def get_updated_config(self, config):
"""
Creates a dictionary with new values whose keys are properly formated
to fit into stormpath config object.
"""
updated_config = {}
for key, value in config.items():
if key.startswith(self.STORMPATH_PREFIX):
stormpath_key = self.MAPPINGS.get(key.split('STORMPATH_')[1])

# Check the format of application information.
if stormpath_key == 'application':
if 'http' in value:
stormpath_key = 'application*href'
else:
stormpath_key = 'application*name'

if stormpath_key:
self.set_key(updated_config, stormpath_key, value)

return updated_config

def process(self, config=None):
if config is None:
config = {}

updated_config = self.get_updated_config(self.config)
_extend_dict(config, updated_config)

return config
8 changes: 0 additions & 8 deletions stormpath_config/strategies/validate_client_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,6 @@ def process(self, config=None):
if not apiKey.get('id') or not apiKey.get('secret'):
raise ValueError('API key ID and secret are required.')

application = config.get('application')
if not application:
raise ValueError('Application cannot be empty.')

href = application.get('href')
if href and '/applications/' not in href:
raise ValueError('Application HREF "%s" is not a valid Stormpath Application HREF.' % href)

web_spa = config.get('web', {}).get('spa', {})
if web_spa and web_spa.get('enabled') and web_spa.get('view') is None:
raise ValueError('SPA mode is enabled but stormpath.web.spa.view isn\'t '
Expand Down
2 changes: 2 additions & 0 deletions tests/assets/secondary_apiKey.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
apiKey.id = SECONDARY_API_KEY_PROPERTIES_ID
apiKey.secret = SECONDARY_API_KEY_PROPERTIES_SECRET
12 changes: 8 additions & 4 deletions tests/strategies/test_edge_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
LoadAPIKeyFromConfigStrategy, \
LoadEnvConfigStrategy, \
LoadFileConfigStrategy, \
ValidateClientConfigStrategy
ValidateClientConfigStrategy, \
MoveAPIKeyToClientAPIKeyStrategy


class EdgeCasesTest(TestCase):
Expand All @@ -35,7 +36,8 @@ def test_config_extending(self):
# constructor.
ExtendConfigStrategy(extend_with=client_config)
]
post_processing_strategies = [LoadAPIKeyFromConfigStrategy()]
post_processing_strategies = [
LoadAPIKeyFromConfigStrategy(), MoveAPIKeyToClientAPIKeyStrategy()]
validation_strategies = [ValidateClientConfigStrategy()]

cl = ConfigLoader(load_strategies, post_processing_strategies, validation_strategies)
Expand Down Expand Up @@ -66,7 +68,8 @@ def test_api_key_file_from_config_with_lesser_loading_order(self):
LoadEnvConfigStrategy(prefix='STORMPATH'),
ExtendConfigStrategy(extend_with={})
]
post_processing_strategies = [LoadAPIKeyFromConfigStrategy()]
post_processing_strategies = [
LoadAPIKeyFromConfigStrategy(), MoveAPIKeyToClientAPIKeyStrategy()]
validation_strategies = [ValidateClientConfigStrategy()]

cl = ConfigLoader(load_strategies, post_processing_strategies, validation_strategies)
Expand Down Expand Up @@ -101,7 +104,8 @@ def test_api_key_from_config_with_lesser_loading_order(self):
# constructor.
ExtendConfigStrategy(extend_with=client_config)
]
post_processing_strategies = [LoadAPIKeyFromConfigStrategy()]
post_processing_strategies = [
LoadAPIKeyFromConfigStrategy(), MoveAPIKeyToClientAPIKeyStrategy()]
validation_strategies = [ValidateClientConfigStrategy()]

cl = ConfigLoader(load_strategies, post_processing_strategies, validation_strategies)
Expand Down
Loading