-
Notifications
You must be signed in to change notification settings - Fork 16
Refactor strategy instantiation for more extensitiliby #1254
Conversation
the shopify extrnal unsafe integration test is failing but i don't think that's related to my changes? looks like unsafe tests were never run on the shopify PR that was merged a few days ago, and looks like the unsafe tests have failed all runs since then, besides for @galvana's draft PR which actually seems like it is amending the shopify issue. @galvana is that accurate? if so, then i think we can ignore the failure here... |
7879b07
to
765b6b2
Compare
@@ -48,6 +45,7 @@ | |||
FIDESOPS_AUTOGENERATED_STORAGE_KEY = "fidesops_autogenerated_storage_destination" | |||
AUTOGENERATED_ACCESS_KEY = "download" | |||
AUTOGENERATED_ERASURE_KEY = "delete" | |||
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.
is it OK to edit this file by hand? i hesitated before doing so since it's a generated migration file...
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 is fine since it's functionally equivalent, we're just cleaning up the constants.
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.
Worth noting that this file wasn't actually auto-generated either, it's a data migration instead of a schema migration and automatically adds several rows to multiple tables in the fidesops application database!
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.
oh, nice - thanks for clarifying that @pattisdr!
@adamsachs, yes, we can ignore the Shopify issues as part of this ticket |
I believe I found the shopify issue and fixed it as part of #1260 🤞 |
50723e7
to
fc0fa76
Compare
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.
Nice work, main thing is fixing up _find_all_strategy_subclasses
- the latest commit broke this, and adding tests around it. I do like this implementation generally.
def test_read_strategies(self, api_client: TestClient): | ||
expected_response = [] | ||
for strategy in MaskingStrategyFactory.get_strategies(): | ||
for strategy in MaskingStrategy.get_strategies(): | ||
expected_response.append(strategy.get_description()) | ||
|
||
response = api_client.get(V1_URL_PREFIX + MASKING_STRATEGY) |
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 test doesn't pick up the error. Currently get_strategies
is broken and returns an empty list, but the expected_response here is also incorrectly an empty list
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.
yeah, i've been a bit uncertain about this test since i first saw it. but i'm also not really sure on the best way to resolve it - i feel like any approach to getting a "true" list of strategies is either going to rely on duplicating the logic of get_strategies()
, or it's going to be prone to false negatives if/when more masking strategies are added to the testing runtime (whether that's core fidesops updates or, potentially, some extended runtime like -plus
).
obviously we need more robust testing for get_strategies()
, as you've correctly pointed out. but what do you think about keeping this as is, and just focusing on firming up the tests around get_strategies()
itself?
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.
The get_strategies() improvement was the main thing, but perhaps asserting that the response is non-empty at least?
c0a48d2
to
5f0241a
Compare
@ethyca/docs-authors would you be able to take a quick look at the docs changes here? this PR is instead of #1163, as we decided on a different implementation approach -- sorry for the double-work! let me know if you've got suggestions. i've also tweaked the description of related docs ticket #1169 to reference this PR, rather than the now outdated #1163 |
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 work @adamsachs 🏆
A small improvement might be adding #1254 (comment), but otherwise this looks good to me.
I'll let your team take care of merging in case there's more left to do -
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.
Just a little tweak - otherwise this looks good!
In order to leverage an implemented masking strategy, the `MaskingStrategy` subclass must be registered with the `MaskingStrategyFactory`. To register a new `MaskingStrategy`, use the `register` decorator on the `MaskingStrategy` subclass definition, as shown in the above example. | ||
|
||
The value passed as the argument to the decorator must be the registered name of the `MaskingStrategy` subclass. This is the same value defined by [callers](#using-fidesops-as-a-masking-service) in the `"masking_strategy"."strategy"` field. | ||
In order to leverage an implemented masking strategy, the `MaskingStrategy` subclass must be imported into the application runtime. Also, the `MaskingStrategy` class must define two class variables: `name`, which is the unique, registered name that callers [callers](#using-fidesops-as-a-masking-service) will use in their `"masking_strategy"."strategy"` field to invoke the strategy; and `configuration_model`, which references the configuration class used to parameterize the strategy. |
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.
In order to leverage an implemented masking strategy, the `MaskingStrategy` subclass must be imported into the application runtime. Also, the `MaskingStrategy` class must define two class variables: `name`, which is the unique, registered name that callers [callers](#using-fidesops-as-a-masking-service) will use in their `"masking_strategy"."strategy"` field to invoke the strategy; and `configuration_model`, which references the configuration class used to parameterize the strategy. | |
In order to leverage an implemented masking strategy, the `MaskingStrategy` subclass must be imported into the application runtime. Also, the `MaskingStrategy` class must define two class variables: `name`, which is the unique, registered name that [callers](#using-fidesops-as-a-masking-service) will use in their `"masking_strategy"."strategy"` field to invoke the strategy; and `configuration_model`, which references the configuration class used to parameterize the strategy. |
A generalized Strategy abstract base class provides generalized getter methods that instantiate strategy subclasses (implementations). These methods rely on the builtin __subclasses__() method to identify Strategy subclasses, which allows for more dynamic and extensible strategy implementation, removing the need for a hardcoded enumeration of supported Strategy implementations. Abstract strategy types inherit from this new abstract base class, and strategy subclasses (implementations) must provide `name` and `configuration_model` attributes that are leveraged by new instantiation mechanism in the abstract base class.
This allows the method to leverage the new `name` class variable rather than relying on a static constant variable.
Strategy factories are no longer needed with refactored Strategy getters. Update the uses (references) of strategy factories throughout the codebase to now rely on the new Strategy getters. Strategy subclasses (implementations) now need to be imported explicitly in __init__.py's because they used to be imported in factory modules. Also remove the old MaskingStrategy registration/factory mechanisms.
Now that the abstract Strategy base class enforces implementation subclasses to have a `name` class attribute, this attribute should be relied upon rather than the arbitrary name constants declared previously. The get_strategy_name() abstract method is also superfluous, as the `name` class attribute can be used as a standardized way to retrieve the strategy name.
The generalized strategy getter now relies upon the `configuration_model` class variable that's on each Strategy. Therefore we no longer need the get_configuration_model() getter on each Strategy subclass.
Update associated tests to make sure the recursion is properly tested
32da9b5
to
48ecfa0
Compare
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 like this approach! I also like how there's a distinction between the different strategy "types" instead of just one combined pool of strategies.
* Instantiate strategies via abstract Strategy base class A generalized Strategy abstract base class provides generalized getter methods that instantiate strategy subclasses (implementations). These methods rely on the builtin __subclasses__() method to identify Strategy subclasses, which allows for more dynamic and extensible strategy implementation, removing the need for a hardcoded enumeration of supported Strategy implementations. Abstract strategy types inherit from this new abstract base class, and strategy subclasses (implementations) must provide `name` and `configuration_model` attributes that are leveraged by new instantiation mechanism in the abstract base class. * Update get_description() to be a class rather than static method This allows the method to leverage the new `name` class variable rather than relying on a static constant variable. * Remove strategy factories and update references Strategy factories are no longer needed with refactored Strategy getters. Update the uses (references) of strategy factories throughout the codebase to now rely on the new Strategy getters. Strategy subclasses (implementations) now need to be imported explicitly in __init__.py's because they used to be imported in factory modules. Also remove the old MaskingStrategy registration/factory mechanisms. * Remove strategy name constants Now that the abstract Strategy base class enforces implementation subclasses to have a `name` class attribute, this attribute should be relied upon rather than the arbitrary name constants declared previously. The get_strategy_name() abstract method is also superfluous, as the `name` class attribute can be used as a standardized way to retrieve the strategy name. * Remove get_configuration_model() abstract method The generalized strategy getter now relies upon the `configuration_model` class variable that's on each Strategy. Therefore we no longer need the get_configuration_model() getter on each Strategy subclass. * Update MaskingStrategy docs with new Strategy functionality * Update changelog * Improve recursion in _find_all_strategy_subclasses * Fix recursion bug when finding all strategies Update associated tests to make sure the recursion is properly tested * Tweak conditional for falsy check * Make get_strategies endpoint test more robust * Fix typo in documentation Co-authored-by: Adam Sachs <[email protected]>
Purpose
Make our strategies more extensible by creating a
Strategy
abstract class using the builtin__subclasses__()
method to find and instantiate Strategy subclases.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 core fidesops codebase. If a developer wanted to implement their own strategy, it required an update to the core fidesops codebase.
With this change, developers outside of core fidesops can implement their own strategy (whether that's an AuthenticationStrategy, MaskingStrategy, PaginationStrategy, or PostProcessorStrategy) and leverage it in the system by simply importing their subclass and ensuring that it defines a unique
name
class variable, along with aconfiguration_model
variable pointing to the strategy's pydantic configuration class. As an example:Changes
Strategy
that defines logic for strategy retrieval and instantiation, through a genericget_strategy
methodname
andconfiguration_model
that are used to identify and instantiate strategy subtypes in a consistent mannerget_strategy
method for the correspondingStrategy
subtypeChecklist
CHANGELOG.md
fileCHANGELOG.md
file is being appended toUnreleased
section 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 Checks
label has been applied, and checks have passed, if this PR touches any external servicesTicket
Fixes #562