Skip to content

Commit 78f6eb3

Browse files
author
Ran Isenberg
committed
cr fixes,. added token validation
1 parent b9571f3 commit 78f6eb3

File tree

5 files changed

+408
-77
lines changed

5 files changed

+408
-77
lines changed
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
"""Advanced feature toggles utility
22
"""
3-
from .configuration_store import ACTION, ConfigurationStore
3+
from .configuration_store import ConfigurationStore
44
from .exceptions import ConfigurationException
5+
from .schema import ACTION, SchemaValidator
56

67
__all__ = [
78
"ConfigurationException",
89
"ConfigurationStore",
910
"ACTION",
11+
"SchemaValidator",
1012
]

aws_lambda_powertools/utilities/feature_toggles/configuration_store.py

Lines changed: 22 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,15 @@
11
# pylint: disable=no-name-in-module,line-too-long
22
import logging
3-
from enum import Enum
43
from typing import Any, Dict, List, Optional
54

65
from botocore.config import Config
76

87
from aws_lambda_powertools.utilities.parameters import AppConfigProvider, GetParameterError, TransformParameterError
98

9+
from . import schema
1010
from .exceptions import ConfigurationException
1111

1212
TRANSFORM_TYPE = "json"
13-
FEATURES_KEY = "features"
14-
RULES_KEY = "rules"
15-
DEFAULT_VAL_KEY = "feature_default_value"
16-
RESTRICTIONS_KEY = "restrictions"
17-
RULE_NAME_KEY = "name"
18-
RULE_DEFAULT_VALUE = "rule_default_value"
19-
RESTRICTION_KEY = "key"
20-
RESTRICTION_VALUE = "value"
21-
RESTRICTION_ACTION = "action"
22-
23-
24-
class ACTION(str, Enum):
25-
EQUALS = "EQUALS"
26-
STARTSWITH = "STARTSWITH"
27-
ENDSWITH = "ENDSWITH"
28-
CONTAINS = "CONTAINS"
29-
3013

3114
logger = logging.getLogger(__name__)
3215

@@ -44,28 +27,26 @@ def __init__(
4427
cache_seconds (int): cache expiration time, how often to call AppConfig to fetch latest configuration
4528
"""
4629
self._cache_seconds = cache_seconds
47-
self.logger = logger
30+
self._logger = logger
4831
self._conf_name = conf_name
32+
self._schema_validator = schema.SchemaValidator(self._logger)
4933
self._conf_store = AppConfigProvider(environment=environment, application=service, config=config)
5034

51-
def _validate_json_schema(self, schema: Dict[str, Any]) -> bool:
52-
#
53-
return True
54-
5535
def _match_by_action(self, action: str, restriction_value: Any, context_value: Any) -> bool:
5636
if not context_value:
5737
return False
5838
mapping_by_action = {
59-
ACTION.EQUALS.value: lambda a, b: a == b,
60-
ACTION.STARTSWITH.value: lambda a, b: a.startswith(b),
61-
ACTION.ENDSWITH.value: lambda a, b: a.endswith(b),
62-
ACTION.CONTAINS.value: lambda a, b: a in b,
39+
schema.ACTION.EQUALS.value: lambda a, b: a == b,
40+
schema.ACTION.STARTSWITH.value: lambda a, b: a.startswith(b),
41+
schema.ACTION.ENDSWITH.value: lambda a, b: a.endswith(b),
42+
schema.ACTION.CONTAINS.value: lambda a, b: a in b,
6343
}
6444

6545
try:
6646
func = mapping_by_action.get(action, lambda a, b: False)
6747
return func(context_value, restriction_value)
68-
except Exception:
48+
except Exception as exc:
49+
self._logger.error(f"caught exception while matching action, action={action}, exception={str(exc)}")
6950
return False
7051

7152
def _handle_rules(
@@ -77,15 +58,17 @@ def _handle_rules(
7758
rules: List[Dict[str, Any]],
7859
) -> bool:
7960
for rule in rules:
80-
rule_name = rule.get(RULE_NAME_KEY, "")
81-
rule_default_value = rule.get(RULE_DEFAULT_VALUE)
61+
rule_name = rule.get(schema.RULE_NAME_KEY, "")
62+
rule_default_value = rule.get(schema.RULE_DEFAULT_VALUE)
8263
is_match = True
83-
restrictions: Dict[str, str] = rule.get(RESTRICTIONS_KEY)
64+
restrictions: Dict[str, str] = rule.get(schema.RESTRICTIONS_KEY)
8465

8566
for restriction in restrictions:
86-
context_value = rules_context.get(restriction.get(RESTRICTION_KEY, ""), "")
67+
context_value = rules_context.get(restriction.get(schema.RESTRICTION_KEY))
8768
if not self._match_by_action(
88-
restriction.get(RESTRICTION_ACTION), restriction.get(RESTRICTION_VALUE), context_value
69+
restriction.get(schema.RESTRICTION_ACTION),
70+
restriction.get(schema.RESTRICTION_VALUE),
71+
context_value,
8972
):
9073
logger.debug(
9174
f"rule did not match action, rule_name={rule_name}, rule_default_value={rule_default_value}, feature_name={feature_name}, context_value={str(context_value)}" # noqa: E501
@@ -121,13 +104,11 @@ def get_configuration(self) -> Dict[str, Any]:
121104
) # parse result conf as JSON, keep in cache for self.max_age seconds
122105
except (GetParameterError, TransformParameterError) as exc:
123106
error_str = f"unable to get AWS AppConfig configuration file, exception={str(exc)}"
124-
logger.error(error_str)
107+
self._logger.error(error_str)
125108
raise ConfigurationException(error_str)
109+
126110
# validate schema
127-
if not self._validate_json_schema(schema):
128-
error_str = "AWS AppConfig schema failed validation"
129-
logger.error(error_str)
130-
raise ConfigurationException(error_str)
111+
self._schema_validator.validate_json_schema(schema)
131112
return schema
132113

133114
def get_feature_toggle(self, *, feature_name: str, rules_context: Dict[str, Any], value_if_missing: bool) -> bool:
@@ -156,15 +137,15 @@ def get_feature_toggle(self, *, feature_name: str, rules_context: Dict[str, Any]
156137
logger.error("unable to get feature toggles JSON, returning provided value_if_missing value") # noqa: E501
157138
return value_if_missing
158139

159-
feature: Dict[str, Dict] = toggles_dict.get(FEATURES_KEY, {}).get(feature_name, None)
140+
feature: Dict[str, Dict] = toggles_dict.get(schema.FEATURES_KEY, {}).get(feature_name, None)
160141
if feature is None:
161142
logger.warning(
162143
f"feature does not appear in configuration, using provided value_if_missing, feature_name={feature_name}, value_if_missing={value_if_missing}" # noqa: E501
163144
)
164145
return value_if_missing
165146

166-
rules_list = feature.get(RULES_KEY, [])
167-
feature_default_value = feature.get(DEFAULT_VAL_KEY)
147+
rules_list = feature.get(schema.RULES_KEY)
148+
feature_default_value = feature.get(schema.FEATURE_DEFAULT_VAL_KEY)
168149
if not rules_list:
169150
# not rules but has a value
170151
logger.debug(
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
from enum import Enum
2+
from typing import Any, Dict
3+
4+
from .exceptions import ConfigurationException
5+
6+
FEATURES_KEY = "features"
7+
RULES_KEY = "rules"
8+
FEATURE_DEFAULT_VAL_KEY = "feature_default_value"
9+
RESTRICTIONS_KEY = "restrictions"
10+
RULE_NAME_KEY = "rule_name"
11+
RULE_DEFAULT_VALUE = "value_when_applies"
12+
RESTRICTION_KEY = "key"
13+
RESTRICTION_VALUE = "value"
14+
RESTRICTION_ACTION = "action"
15+
16+
17+
class ACTION(str, Enum):
18+
EQUALS = "EQUALS"
19+
STARTSWITH = "STARTSWITH"
20+
ENDSWITH = "ENDSWITH"
21+
CONTAINS = "CONTAINS"
22+
23+
24+
class SchemaValidator:
25+
def __init__(self, logger: object):
26+
self._logger = logger
27+
28+
def _raise_conf_exc(self, error_str: str) -> None:
29+
self._logger.error(error_str)
30+
raise ConfigurationException(error_str)
31+
32+
def _validate_restriction(self, rule_name: str, restriction: Dict[str, str]) -> None:
33+
if not restriction or not isinstance(restriction, dict):
34+
self._raise_conf_exc(f"invalid restriction type, not a dictionary, rule_name={rule_name}")
35+
action = restriction.get(RESTRICTION_ACTION, "")
36+
if action not in [ACTION.EQUALS.value, ACTION.STARTSWITH.value, ACTION.ENDSWITH.value, ACTION.CONTAINS.value]:
37+
self._raise_conf_exc(f"invalid action value, rule_name={rule_name}, action={action}")
38+
key = restriction.get(RESTRICTION_KEY, "")
39+
if not key or not isinstance(key, str):
40+
self._raise_conf_exc(f"invalid key value, key has to be a non empty string, rule_name={rule_name}")
41+
value = restriction.get(RESTRICTION_VALUE, "")
42+
if not value:
43+
self._raise_conf_exc(f"missing restriction value, rule_name={rule_name}")
44+
45+
def _validate_rule(self, feature_name: str, rule: Dict[str, Any]) -> None:
46+
if not rule or not isinstance(rule, dict):
47+
self._raise_conf_exc(f"feature rule is not a dictionary, feature_name={feature_name}")
48+
rule_name = rule.get(RULE_NAME_KEY)
49+
if not rule_name or rule_name is None or not isinstance(rule_name, str):
50+
self._raise_conf_exc(f"invalid rule_name, feature_name={feature_name}")
51+
rule_default_value = rule.get(RULE_DEFAULT_VALUE)
52+
if rule_default_value is None or not isinstance(rule_default_value, bool):
53+
self._raise_conf_exc(f"invalid rule_default_value, rule_name={rule_name}")
54+
restrictions = rule.get(RESTRICTIONS_KEY, {})
55+
if not restrictions or not isinstance(restrictions, list):
56+
self._raise_conf_exc(f"invalid restrictions, rule_name={rule_name}")
57+
# validate restrictions
58+
for restriction in restrictions:
59+
self._validate_restriction(rule_name, restriction)
60+
61+
def _validate_feature(self, feature_name: str, feature_dict_def: Dict[str, Any]) -> None:
62+
if not feature_dict_def or not isinstance(feature_dict_def, dict):
63+
self._raise_conf_exc(f"invalid AWS AppConfig JSON schema detected, feature {feature_name} is invalid")
64+
feature_default_value = feature_dict_def.get(FEATURE_DEFAULT_VAL_KEY)
65+
if feature_default_value is None or not isinstance(feature_default_value, bool):
66+
self._raise_conf_exc(f"missing feature_default_value for feature, feature_name={feature_name}")
67+
# validate rules
68+
rules = feature_dict_def.get(RULES_KEY, [])
69+
if not rules:
70+
return
71+
if not isinstance(rules, list):
72+
self._raise_conf_exc(f"feature rules is not a list, feature_name={feature_name}")
73+
for rule in rules:
74+
self._validate_rule(feature_name, rule)
75+
76+
def validate_json_schema(self, schema: Dict[str, Any]) -> None:
77+
if not isinstance(schema, dict):
78+
self._raise_conf_exc("invalid AWS AppConfig JSON schema detected, root schema is not a dictionary")
79+
features_dict: Dict = schema.get(FEATURES_KEY)
80+
if not isinstance(features_dict, dict):
81+
self._raise_conf_exc("invalid AWS AppConfig JSON schema detected, missing features dictionary")
82+
for feature_name, feature_dict_def in features_dict.items():
83+
self._validate_feature(feature_name, feature_dict_def)

0 commit comments

Comments
 (0)