diff --git a/README.md b/README.md index 3eb5cab..7e7a4cf 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,7 @@ Client.build_context( snapshot_location='./snapshot/', snapshot_auto_update_interval=3, silent_mode='5m', + restrict_relay=True, throttle_max_workers=2, regex_max_black_list=10, regex_max_time_limit=100, @@ -154,6 +155,7 @@ switcher = Client.get_switcher() | `snapshot_location` | `str` | Directory for snapshot files | `'./snapshot/'` | | `snapshot_auto_update_interval` | `int` | Auto-update interval in seconds (0 = disabled) | `0` | | `silent_mode` | `str` | Silent mode retry time (e.g., '5m' for 5 minutes) | `None` | +| `restrict_relay` | `bool` | 🚧 TODO - Enable relay restrictions in local mode | `True` | | `throttle_max_workers` | `int` | Max workers for throttling feature checks | `None` | | `regex_max_black_list` | `int` | Max cached entries for failed regex | `100` | | `regex_max_time_limit` | `int` | Regex execution time limit (ms) | `3000` | diff --git a/switcher_client/client.py b/switcher_client/client.py index ad09397..fe217d8 100644 --- a/switcher_client/client.py +++ b/switcher_client/client.py @@ -83,7 +83,8 @@ def get_switcher(key: Optional[str] = None) -> Switcher: if persisted_switcher is not None: return persisted_switcher - switcher = Switcher(Client._context, key_value) + switcher = Switcher(Client._context, key_value) \ + .restrict_relay(Client._context.options.restrict_relay) if key_value != '': Client._switcher[key_value] = switcher diff --git a/switcher_client/lib/globals/global_context.py b/switcher_client/lib/globals/global_context.py index 9e065ca..c789ae2 100644 --- a/switcher_client/lib/globals/global_context.py +++ b/switcher_client/lib/globals/global_context.py @@ -4,6 +4,7 @@ DEFAULT_LOCAL = False DEFAULT_LOGGER = False DEFAULT_FREEZE = False +DEFAULT_RESTRICT_RELAY = True DEFAULT_REGEX_MAX_BLACKLISTED = 100 DEFAULT_REGEX_MAX_TIME_LIMIT = 3000 @@ -18,14 +19,16 @@ class ContextOptions: :param throttle_max_workers: The maximum number of workers to use for background refresh when throttle is enabled. If None, the default value is based on the number of CPUs. Default is None :param regex_max_black_list: The maximum number of blacklisted regex inputs. If not set, it will use the default value of 100 :param regex_max_time_limit: The maximum time limit in milliseconds for regex matching. If not set, it will use the default value of 3000 ms + :param restrict_relay: When enabled it will restrict the use of relay when local is enabled. Default is True """ def __init__(self, - local = DEFAULT_LOCAL, - logger = DEFAULT_LOGGER, - freeze = DEFAULT_FREEZE, - regex_max_black_list = DEFAULT_REGEX_MAX_BLACKLISTED, - regex_max_time_limit = DEFAULT_REGEX_MAX_TIME_LIMIT, + local: bool = DEFAULT_LOCAL, + logger: bool = DEFAULT_LOGGER, + freeze: bool = DEFAULT_FREEZE, + regex_max_black_list: int = DEFAULT_REGEX_MAX_BLACKLISTED, + regex_max_time_limit: int = DEFAULT_REGEX_MAX_TIME_LIMIT, + restrict_relay: bool = DEFAULT_RESTRICT_RELAY, snapshot_location: Optional[str] = None, snapshot_auto_update_interval: Optional[int] = None, silent_mode: Optional[str] = None, @@ -36,6 +39,7 @@ def __init__(self, self.snapshot_location = snapshot_location self.snapshot_auto_update_interval = snapshot_auto_update_interval self.silent_mode = silent_mode + self.restrict_relay = restrict_relay self.throttle_max_workers = throttle_max_workers self.regex_max_black_list = regex_max_black_list self.regex_max_time_limit = regex_max_time_limit diff --git a/switcher_client/lib/resolver.py b/switcher_client/lib/resolver.py index 25782fd..82db442 100644 --- a/switcher_client/lib/resolver.py +++ b/switcher_client/lib/resolver.py @@ -46,6 +46,9 @@ def _check_config(config: Config, switcher: SwitcherData) -> ResultDetail: if config.activated is False: return ResultDetail.disabled("Config disabled") + if Resolver._has_relay_enabled(config) and switcher._restrict_relay: + return ResultDetail.disabled("Config has relay enabled") + if config.strategies is not None and len(config.strategies) > 0: return Resolver._check_strategy(config.strategies, switcher._input) @@ -81,4 +84,9 @@ def _check_strategy_config(strategy_config: StrategyConfig, entry: list[Entry]) @staticmethod def _is_strategy_fulfilled(strategy_entry: list[Entry], strategy_config: StrategyConfig) -> bool: """ Determines if the strategy conditions are fulfilled based on the entries and configuration. """ - return len(strategy_entry) > 0 and process_operation(strategy_config, strategy_entry[0].input) is True \ No newline at end of file + return len(strategy_entry) > 0 and process_operation(strategy_config, strategy_entry[0].input) is True + + @staticmethod + def _has_relay_enabled(config: Config) -> bool: + """ Checks if the config has relay enabled. """ + return config.relay.activated if config.relay else False diff --git a/switcher_client/switcher_data.py b/switcher_client/switcher_data.py index 0fe9fe2..18b4791 100644 --- a/switcher_client/switcher_data.py +++ b/switcher_client/switcher_data.py @@ -15,6 +15,7 @@ def __init__(self, context: Context,key: Optional[str] = None): self._next_refresh_time = 0 # timestamp self._default_result = None self._force_remote = False + self._restrict_relay = True def check(self, strategy_type: str, input: str)-> Self: """ Adds a strategy for validation """ @@ -67,4 +68,9 @@ def remote(self, force_remote: bool = True) -> Self: def default_result(self, result: bool) -> Self: """ Sets the default result for the switcher """ self._default_result = result + return self + + def restrict_relay(self, restrict: bool = True) -> Self: + """ Allow local snapshots to ignore or require Relay verification """ + self._restrict_relay = restrict return self \ No newline at end of file diff --git a/tests/test_switcher_local.py b/tests/test_switcher_local.py index 4899457..438c08a 100644 --- a/tests/test_switcher_local.py +++ b/tests/test_switcher_local.py @@ -4,7 +4,7 @@ from switcher_client.client import Client, ContextOptions from switcher_client.errors import LocalCriteriaError -from switcher_client.lib.utils.timed_match.timed_match import DEFAULT_REGEX_MAX_BLACKLISTED, DEFAULT_REGEX_MAX_TIME_LIMIT +from switcher_client.lib.globals.global_context import DEFAULT_REGEX_MAX_BLACKLISTED, DEFAULT_REGEX_MAX_TIME_LIMIT, DEFAULT_RESTRICT_RELAY async_error = None @@ -222,11 +222,40 @@ def test_local_no_snapshot(): except LocalCriteriaError as e: assert str(e) == "Snapshot not loaded. Try to use 'Client.load_snapshot()'" +def test_local_retrict_relay(): + """ Should return disabled when Relay is enabled (using default restrict_relay behavior) """ + + # given + given_context('tests/snapshots') + snapshot_version = Client.load_snapshot() + + switcher = Client.get_switcher() + + # test + assert snapshot_version == 1 + assert switcher.is_on('USECASE103') is False # relay enabled + assert switcher.restrict_relay(False).is_on('USECASE103') is True # relay enabled (force override) + assert switcher.is_on('USECASE104') is True # relay disabled + +def test_local_restrict_relay_override(): + """ Should return enabled when Relay is enabled but restrict_relay is overridden to False """ + + # given + given_context('tests/snapshots', restrict_relay=False) + snapshot_version = Client.load_snapshot() + + switcher = Client.get_switcher() + + # test + assert snapshot_version == 1 + assert switcher.is_on('USECASE103') + # Helpers def given_context(snapshot_location: str, environment: str = 'default', regex_max_black_list = DEFAULT_REGEX_MAX_BLACKLISTED, - regex_max_time_limit = DEFAULT_REGEX_MAX_TIME_LIMIT): + regex_max_time_limit = DEFAULT_REGEX_MAX_TIME_LIMIT, + restrict_relay = DEFAULT_RESTRICT_RELAY): Client.build_context( domain='Playground', environment=environment, @@ -235,6 +264,7 @@ def given_context(snapshot_location: str, environment: str = 'default', logger=True, snapshot_location=snapshot_location, regex_max_black_list=regex_max_black_list, - regex_max_time_limit=regex_max_time_limit + regex_max_time_limit=regex_max_time_limit, + restrict_relay=restrict_relay ) ) \ No newline at end of file