-
Notifications
You must be signed in to change notification settings - Fork 16
unified strategy factory with decorator to register strategies #1163
unified strategy factory with decorator to register strategies #1163
Conversation
…act Strategy class with class variables to declare strategy name and configuration model class
| strategy = MaskingStrategyFactory.get_strategy( | ||
| masking_strategy.strategy, masking_strategy.configuration | ||
| masking_strategy_spec = request.masking_strategy | ||
| masking_strategy: MaskingStrategy = strategy( # type: ignore |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i don't like having to suppress the type checker here, but i couldn't find a better way to get around this. by using a single, global factory, we lose the ability to strongly type the return type of the strategy getter -- since the factory only knows about the abstract Strategy class, it must return a Strategy here and not a more specific subclass. i couldn't find a clean way for the calling ("client") code to specify what subtype of Strategy it was expecting back.
also noting that in previous iterations discussed with @PSalant726, i'd had an approach where there was not a single global factory, but instead an abstract factory with generics, and then that factory was subclassed by each corresponding Strategy subclass (e.g. MaskingStrategyFactory, PaginationStrategyFactory). the subclassed factory just provided a specific type to the generic that was declared in the abstract factory. i believe that approach would allow us to strongly type the return types here, as we'd be specifically retrieving a strategy from (in this case) the MaskingStrategyFactory (rather than a global strategy factory). but we decided against that approach, since it led to bloat and a somewhat confusing implementation of StrategyFactory subclasses that did not provide any additional functionality beyond typing. for more information, e6416f4 shows what this approach would generally look like
… is no longer needed with updated pylint version
|
@ethyca/docs-authors i updated the i don't think these changes affect any other documentation - i don't believe we've yet documented how a developer can "implement their own" |
conceptualshark
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good with the docs updates - agree that a new ticket for docs to cover some of the ways this can be used/extended is a good idea, if you haven't made it yet!
created #1169 to track this doc request. no real priority on it afaik |
pattisdr
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pausing review here, there are issues in bringing up the webserver itself with this change and generally I favor some of the alternatives you mentioned in the original issue. I also don't know if all of these strategies should be lumped together.
| ConnectorTemplate, | ||
| create_connection_config_from_template_no_save, | ||
| create_dataset_config_from_template, | ||
| load_registry, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
load_registry runs when we bring up the webserver - this now gives you a lot of validation errors:
webserver_1 | Traceback (most recent call last):
webserver_1 | File "/usr/local/bin/fidesops", line 8, in <module>
webserver_1 | sys.exit(cli())
webserver_1 | File "/usr/local/lib/python3.9/site-packages/click/core.py", line 1130, in __call__
webserver_1 | return self.main(*args, **kwargs)
webserver_1 | File "/usr/local/lib/python3.9/site-packages/click/core.py", line 1055, in main
webserver_1 | rv = self.invoke(ctx)
webserver_1 | File "/usr/local/lib/python3.9/site-packages/click/core.py", line 1657, in invoke
webserver_1 | return _process_result(sub_ctx.command.invoke(sub_ctx))
webserver_1 | File "/usr/local/lib/python3.9/site-packages/click/core.py", line 1404, in invoke
webserver_1 | return ctx.invoke(self.callback, **ctx.params)
webserver_1 | File "/usr/local/lib/python3.9/site-packages/click/core.py", line 760, in invoke
webserver_1 | return __callback(*args, **kwargs)
webserver_1 | File "/usr/local/lib/python3.9/site-packages/click/decorators.py", line 26, in new_func
webserver_1 | return f(get_current_context(), *args, **kwargs)
webserver_1 | File "/fidesops/src/fidesops/ops/cli.py", line 25, in webserver
webserver_1 | start_webserver()
webserver_1 | File "/fidesops/src/fidesops/main.py", line 234, in start_webserver
webserver_1 | load_registry(registry_file)
webserver_1 | File "/fidesops/src/fidesops/ops/service/connectors/saas/connector_registry_service.py", line 133, in load_registry
webserver_1 | _registry = ConnectorRegistry.parse_obj(load_toml([config_file]))
webserver_1 | File "pydantic/main.py", line 521, in pydantic.main.BaseModel.parse_obj
webserver_1 | File "pydantic/main.py", line 341, in pydantic.main.BaseModel.__init__
webserver_1 | pydantic.error_wrappers.ValidationError: 29 validation errors for ConnectorRegistry
webserver_1 | __root__ -> datadog -> config -> endpoints -> 0 -> requests -> read -> __root__
webserver_1 | Strategy 'link' does not exist. Valid strategies are [aes_encrypt, hash, hmac, null_rewrite, random_string_rewrite, string_rewrite, oauth2] (type=value_error.nosuchstrategyexception)
webserver_1 | __root__ -> hubspot -> config -> endpoints -> 0 -> requests -> read -> __root__
webserver_1 | Strategy 'link' does not exist. Valid strategies are [aes_encrypt, hash, hmac, null_rewrite, random_string_rewrite, string_rewrite, oauth2] (type=value_error.nosuchstrategyexception)
webserver_1 | __root__ -> hubspot -> config -> endpoints -> 1 -> requests -> read -> __root__
webserver_1 | Strategy 'link' does not exist. Valid strategies are [aes_encrypt, hash, hmac, null_rewrite, random_string_rewrite, string_rewrite, oauth2] (type=value_error.nosuchstrategyexception)
webserver_1 | __root__ -> hubspot -> config -> endpoints -> 3 -> requests -> read -> __root__
webserver_1 | Strategy 'link' does not exist. Valid strategies are [aes_encrypt, hash, hmac, null_rewrite, random_string_rewrite, string_rewrite, oauth2] (type=value_error.nosuchstrategyexception)
webserver_1 | __root__ -> mailchimp -> config -> endpoints -> 1 -> requests -> read -> __root__
webserver_1 | Strategy 'offset' does not exist. Valid strategies are [aes_encrypt, hash, hmac, null_rewrite, random_string_rewrite, string_rewrite, oauth2] (type=value_error.nosuchstrategyexception)
webserver_1 | __root__ -> outreach -> config -> endpoints -> 1 -> requests -> read -> __root__
webserver_1 | Strategy 'link' does not exist. Valid strategies are [aes_encrypt, hash, hmac, null_rewrite, random_string_rewrite, string_rewrite, oauth2] (type=value_error.nosuchstrategyexception)
webserver_1 | __root__ -> segment -> config -> endpoints -> 1 -> requests -> read -> __root__
webserver_1 | Strategy 'link' does not exist. Valid strategies are [aes_encrypt, hash, hmac, null_rewrite, random_string_rewrite, string_rewrite, oauth2] (type=value_error.nosuchstrategyexception)
webserver_1 | __root__ -> segment -> config -> endpoints -> 2 -> requests -> read -> __root__
webserver_1 | Strategy 'link' does not exist. Valid strategies are [aes_encrypt, hash, hmac, null_rewrite, random_string_rewrite, string_rewrite, oauth2] (type=value_error.nosuchstrategyexception)
webserver_1 | __root__ -> segment -> config -> endpoints -> 3 -> requests -> read -> __root__
webserver_1 | Strategy 'link' does not exist. Valid strategies are [aes_encrypt, hash, hmac, null_rewrite, random_string_rewrite, string_rewrite, oauth2] (type=value_error.nosuchstrategyexception)
webserver_1 | __root__ -> sentry -> config -> endpoints -> 0 -> requests -> read -> __root__
webserver_1 | Strategy 'link' does not exist. Valid strategies are [aes_encrypt, hash, hmac, null_rewrite, random_string_rewrite, string_rewrite, oauth2] (type=value_error.nosuchstrategyexception)
webserver_1 | __root__ -> sentry -> config -> endpoints -> 2 -> requests -> read -> __root__
webserver_1 | Strategy 'link' does not exist. Valid strategies are [aes_encrypt, hash, hmac, null_rewrite, random_string_rewrite, string_rewrite, oauth2] (type=value_error.nosuchstrategyexception)
webserver_1 | __root__ -> sentry -> config -> endpoints -> 3 -> requests -> read -> __root__
webserver_1 | Strategy 'link' does not exist. Valid strategies are [aes_encrypt, hash, hmac, null_rewrite, random_string_rewrite, string_rewrite, oauth2] (type=value_error.nosuchstrategyexception)
webserver_1 | __root__ -> sentry -> config -> endpoints -> 4 -> requests -> read -> __root__
webserver_1 | Strategy 'link' does not exist. Valid strategies are [aes_encrypt, hash, hmac, null_rewrite, random_string_rewrite, string_rewrite, oauth2] (type=value_error.nosuchstrategyexception)
webserver_1 | __root__ -> sentry -> config -> endpoints -> 5 -> requests -> read -> __root__
webserver_1 | Strategy 'link' does not exist. Valid strategies are [aes_encrypt, hash, hmac, null_rewrite, random_string_rewrite, string_rewrite, oauth2] (type=value_error.nosuchstrategyexception)
webserver_1 | __root__ -> stripe -> config -> endpoints -> 1 -> requests -> read -> __root__
webserver_1 | Strategy 'cursor' does not exist. Valid strategies are [aes_encrypt, hash, hmac, null_rewrite, random_string_rewrite, string_rewrite, oauth2] (type=value_error.nosuchstrategyexception)
webserver_1 | __root__ -> stripe -> config -> endpoints -> 2 -> requests -> read -> __root__
webserver_1 | Strategy 'cursor' does not exist. Valid strategies are [aes_encrypt, hash, hmac, null_rewrite, random_string_rewrite, string_rewrite, oauth2] (type=value_error.nosuchstrategyexception)
webserver_1 | __root__ -> stripe -> config -> endpoints -> 3 -> requests -> read -> __root__
webserver_1 | Strategy 'cursor' does not exist. Valid strategies are [aes_encrypt, hash, hmac, null_rewrite, random_string_rewrite, string_rewrite, oauth2] (type=value_error.nosuchstrategyexception)
webserver_1 | __root__ -> stripe -> config -> endpoints -> 4 -> requests -> read -> __root__
webserver_1 | Strategy 'cursor' does not exist. Valid strategies are [aes_encrypt, hash, hmac, null_rewrite, random_string_rewrite, string_rewrite, oauth2] (type=value_error.nosuchstrategyexception)
webserver_1 | __root__ -> stripe -> config -> endpoints -> 5 -> requests -> read -> __root__
webserver_1 | Strategy 'cursor' does not exist. Valid strategies are [aes_encrypt, hash, hmac, null_rewrite, random_string_rewrite, string_rewrite, oauth2] (type=value_error.nosuchstrategyexception)
webserver_1 | __root__ -> stripe -> config -> endpoints -> 6 -> requests -> read -> __root__
webserver_1 | Strategy 'cursor' does not exist. Valid strategies are [aes_encrypt, hash, hmac, null_rewrite, random_string_rewrite, string_rewrite, oauth2] (type=value_error.nosuchstrategyexception)
webserver_1 | __root__ -> stripe -> config -> endpoints -> 7 -> requests -> read -> __root__
webserver_1 | Strategy 'cursor' does not exist. Valid strategies are [aes_encrypt, hash, hmac, null_rewrite, random_string_rewrite, string_rewrite, oauth2] (type=value_error.nosuchstrategyexception)
webserver_1 | __root__ -> stripe -> config -> endpoints -> 8 -> requests -> read -> __root__
webserver_1 | Strategy 'cursor' does not exist. Valid strategies are [aes_encrypt, hash, hmac, null_rewrite, random_string_rewrite, string_rewrite, oauth2] (type=value_error.nosuchstrategyexception)
webserver_1 | __root__ -> stripe -> config -> endpoints -> 9 -> requests -> read -> __root__
webserver_1 | Strategy 'cursor' does not exist. Valid strategies are [aes_encrypt, hash, hmac, null_rewrite, random_string_rewrite, string_rewrite, oauth2] (type=value_error.nosuchstrategyexception)
webserver_1 | __root__ -> stripe -> config -> endpoints -> 10 -> requests -> read -> __root__
webserver_1 | Strategy 'cursor' does not exist. Valid strategies are [aes_encrypt, hash, hmac, null_rewrite, random_string_rewrite, string_rewrite, oauth2] (type=value_error.nosuchstrategyexception)
webserver_1 | __root__ -> stripe -> config -> endpoints -> 11 -> requests -> read -> __root__
webserver_1 | Strategy 'cursor' does not exist. Valid strategies are [aes_encrypt, hash, hmac, null_rewrite, random_string_rewrite, string_rewrite, oauth2] (type=value_error.nosuchstrategyexception)
webserver_1 | __root__ -> stripe -> config -> endpoints -> 12 -> requests -> read -> __root__
webserver_1 | Strategy 'cursor' does not exist. Valid strategies are [aes_encrypt, hash, hmac, null_rewrite, random_string_rewrite, string_rewrite, oauth2] (type=value_error.nosuchstrategyexception)
webserver_1 | __root__ -> zendesk -> config -> endpoints -> 1 -> requests -> read -> __root__
webserver_1 | Strategy 'link' does not exist. Valid strategies are [aes_encrypt, hash, hmac, null_rewrite, random_string_rewrite, string_rewrite, oauth2] (type=value_error.nosuchstrategyexception)
webserver_1 | __root__ -> zendesk -> config -> endpoints -> 2 -> requests -> read -> __root__
webserver_1 | Strategy 'link' does not exist. Valid strategies are [aes_encrypt, hash, hmac, null_rewrite, random_string_rewrite, string_rewrite, oauth2] (type=value_error.nosuchstrategyexception)
webserver_1 | __root__ -> zendesk -> config -> endpoints -> 3 -> requests -> read -> __root__
webserver_1 | Strategy 'link' does not exist. Valid strategies are [aes_encrypt, hash, hmac, null_rewrite, random_string_rewrite, string_rewrite, oauth2] (type=value_error.nosuchstrategyexception)
fidesops_webserver_1 exited with code 1
mongodb_example_1 | {"t":{"$date":"2022-08-29T16:19:07.403+00:00"},"s":"I", "c":"STORAGE", "id":22430, "ctx":"Checkpointer","msg":"WiredTiger message","attr":{"message":"[1661789947:403539][1:0xffff7d17ad00], WT_SESSION.checkpoint: [WT_VERB_CHECKPOINT_PROGRESS] saving checkpoint snapshot min: 36, snapshot max: 36 snapshot count: | strategy_factory = StrategyFactory() | ||
| register = strategy_factory.register() | ||
| strategy = strategy_factory.strategy | ||
| strategies = strategy_factory.strategies |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As you're probably aware, strategies are only available here if they have been imported into a file that was already loaded prior to calling this function. I would expect "strategies" to include all classes marked by the "register" decorator.
As an example, if you open up a fidesops shell, start python and import the strategies callable, the strategies are empty.
Another example, if you look at what strategies are available in the strategies() call in tests/ops/api/v1/endpoints/test_masking_endpoints.py::TestGetMaskingStrategies::test_read_strategies, it shows 7 while it looks like you have about 15 decorated. These seven come from strategies imported in application fixtures and in src/fidesops/ops/service/masking/__init__.py.
At minimum, I might import the strategies in the individual init files, similar to what was done in fidesops/ops/service/masking/init.py, (so add imports to fidesops/ops/service/processors/post_processor_strategy/init.py, fidesops/ops/service/pagination/init.py, fidesops/ops/service/authentication/init.py, etc.). However, this still seems brittle.
|
|
||
| def get_strategy_name(self) -> str: | ||
| return STRATEGY_NAME | ||
| super().__init__(configuration) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally, it feels like we're trying to force all of these disparate strategies to all be registered in the same way, when they are distinct types of objects. Their individual configurations are hardly related and I think it would cause confusion for someone trying to create their own strategy.
I might recommend keeping them separate, and allowing a user to creating their own "masking strategy", "pagination strategy", "post processor strategy", or "authentication strategy".
| # this constant is kept around because it is referenced in generated alembic migrations | ||
| STRING_REWRITE_STRATEGY_NAME = "string_rewrite" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of importing this into that migration then, I might just move this string directly into the migration.
| def register(self) -> Callable[[Type[Strategy]], Type[Strategy]]: | ||
| def wrapper( | ||
| strategy_class: Type[Strategy], | ||
| ) -> Type[Strategy]: | ||
|
|
||
| _validate_strategy_class(strategy_class) | ||
|
|
||
| name = strategy_class.name | ||
| logger.debug( | ||
| ("Registering new strategy '%s' under name '%s'", strategy_class, name) | ||
| ) | ||
|
|
||
| if name in self.registry: | ||
| logger.warning( | ||
| ( | ||
| "Strategy with name '%s' already exists. It previously referred to class '%s', but will now refer to '%s'", | ||
| name, | ||
| self.registry[name], | ||
| strategy_class, | ||
| ) | ||
| ) | ||
|
|
||
| self.registry[name] = strategy_class | ||
| self.valid_strategies = ", ".join(self.registry.keys()) | ||
| return self.registry[name] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of this register decorator, you might explore Metaclasses, or the __subclasses__ method that returns all child classes. I see you mentioned these as alternatives in the original issue. I might lean on something more built-in, the register decorator adds an extra step and makes me think on first glance that it's magically going to add the strategy to the registry with its usage alone, when the strategy still needs to be imported to be registered.
| strategy = strategy_factory.strategy | ||
| strategies = strategy_factory.strategies |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't feel very Pythonic to me, but if we're exposing it, I'd at least show they are callable types, and rename to something like get_strategy and get_strategies.
|
closing this PR, as we went with a different approach and opened up #1254 instead. |
Purpose
Make our strategies more extensible by using a unified strategy factory that allows for decorator-based registration.
Before this change, many of our strategies (with the exception of
MaskingStrategys, which had been refactored in #560) were registered by means of a hardcoded enums in the corefidesopscodebase. If a developer wanted to implement their own strategy, it required an update to the corefidesopscodebase.With this change, developers outside of core
fidesopscan implement their own strategy (whether that's anAuthenticationStrategy,MaskingStrategy,PaginationStrategy, orPostProcessorStrategy) and leverage it in the system by simply including the@registerdecorator on their strategy class, and ensuring it has the expected class variables and constructor as specified in theStrategyabstract base class. As an example:Additionally, new abstract
Strategysubtypes (e.g. aPreProcessorStrategymodule, if the need arises) can be defined and hooked into this framework simply by subclassing the newly addedStrategyabstract base class. A new factory does not need to be defined.Changes
registerclass decorator that can be used as a hook to dynamically registerStrategyimplementations*StrategyFactoryclasses as they are no longer neededStrategyabstract base class that provides the general specification for what's needed in aStrategyimplementation, which at this point is just:nameandconfiguration_modelwhich define theStrategyimplementations name to be registered under, and pydantic model configuration class to use, respectivelyconfiguration_modelclass variableStrategyimplementations so that they are compatible with this new framework. there should be no user-facing effects of these changes, just rewiring on the backend.Checklist
CHANGELOG.mdfileCHANGELOG.mdfile is being appended toUnreleasedsection in an appropriate category. Add a new category from the list at the top of the file if the needed one isn't already there.Run Unsafe PR Checkslabel has been applied, and checks have passed, if this PR touches any external servicesTicket
Fixes #562