Problem/Motivation

Config entities seem to not mirror the behaviour enforced by underlying configuration API.

The underlying configuration API behaviour is described as:

  • Normally you would get the configuration as "immutable", so it can not saved & in fact will even throw an exception when you still try it (But it will contain corresponding config overrides if any are specified)
  • If you want to change the configuration you need to get it as "editable", so you can save it(But it will not contain any overrides)

Configuration API

But for config entities this is not described as clearly. The only information you get is that for "admin"- routes (/admin/*) if config entities are used as route parameters, their values are ensured to be not-overriden (like e.g. routes that use an entity form); but only on those routes.

Config enties actually do not distinguish between a "immutable" & "editable" variants.
This allows for the following two code flows:
a) Loading an not-overriden version of the config entity, change it, save it:

  • load/change: the entity contains not-overriden, possibly changed, values
  • preSave: the not-overriden values can be compared agains the "original" version (the "original" does contain overriden values)
  • save: the not-overriden values may be cast to match the configuration schema & are stored as configuration
  • postSave: the not-overriden values (which may have been cast to match the schema) can be compared agains the "original" version (the "original" does contain overriden values)

b) Loading an overriden version of the config entity, change it, save it

  • load/change: the entity contains overriden (possibly changed) values (the changed values might be the ones that were actually overriden or not)
  • preSave: the overriden values can be compared agains the "original" version (the "original" does contain overriden values)
  • save: the overriden values may be cast to match the configuration schema & are stored as configuration (At this point overriden values might be stored although they were not actively modified)
  • postSave: the overriden values (which may have been cast to match the schema) can be compared agains the "original" version (the "original" does contain overriden values)

This shows on one hand that as @druken monkey described above the code in the preSave/postSave hooks/entity methods may be called with overriden or not overriden values. On the other hand it shows a bug that allows overriden values to accidentaly be leaked into the actual "editable" configuration.

Proposed resolution

  • Introduce a flag to config entities like: isEditable / isImmutable / mayContainOverrides
  • Per default load config entities as: !isEditable / isImmutable / mayContainOverrides
  • Disallow saving entities in the ConfigEntity storage (by throwing an exception) which are: !isEditable / isImmutable / mayContainOverrides
  • Maybe add a method on config entities / simply use the ConfigEntityStorage::loadOverrideFree to get instances that are flagged as: isEditable / !isImmutable / !mayContainOverrides
  • Since all config saved would now be ensured as not-overriden, preSave hooks/entity methods should get an not-overriden original
  • postSave hooks/entity methods would also get a not-overriden original
  • If someone needs an overriden original in preSave, they could simply load an overriden version (as the changes are not yet persisted)
  • If someone needs an overriden original in a postSave hook / method, they could could theoretically set it as a seperate Field like maybe 'overridenOriginal' in a corresponding preSave hook/ method so they cant it get's passed along to postSave hook/ method
  • A generic 'overrideOriginal' solution would also be possible but would need to be implemented at the storage level (the "original" should be unset at the end, but because entity->postSave ist called before the entity hooks this could not be done in the config entity class)

This way the beghaviour would be more in line with the underlying configuration API & And preSave/postSave hooks/entity methods work with a more predictable context.

Remaining tasks

  • Introduce a new base class, interface & storage
  • Decide what to do with the existing config entity related classes (merge changes into them, deprecate them, change them to not allow config overrides ...)

API changes

  • Introduces new Classes & an Interface / expands the existing ones (OverrideSupportingConfigEntityInterface, OverrideSupportingConfigEntityBase & OverrideSupportingConfigEntityStorage)/li>

Data model changes

No changes, the overriden data only resides in the class and is not persisted.

Original report by drunken monkey

In the Search API (#2682369: Fix problems with overridden config entities) we've discovered problems when using config overrides with config entities, which I think could affect other modules as well, and might therefore be better solved in Core than in each of them separately.

The root cause of the problem is basically that an entity update will mix non-overridden and overridden entities in non-obvious ways.
Specifically, config entities are by default not overridden when loaded for admin routes, where most of the changes probably occur. But when saving such an entity, the original that is loaded is loaded with overrides, so when any pre-/post-save code tries to assess the changes done to the entity, it will inevitably end up with wrong results when comparing overridden properties.

Ideally, I think, both the "current" and the "original" entity used would show all the overridden values, so that really only those properties that effectively changed their value are actually detected as changes.
However, especially for the pre-save code, this is probably not that easy to do, and doing it just for post-save might just add more confusion.

So, this is more meant as the start of a discussion, or a request for other solutions.
Do you even agree that this is a problem? And, if so, how can we solve it?

Comments

drunken monkey created an issue. See original summary.

Alumei’s picture

Problem

I think the main problem is actually, that config entities do not mirror the behaviour enforced by underlying configuration API.

The underlying configuration API behaviour is described as:

  • Normally you would get the configuration as "immutable", so it can not saved & in fact will even throw an exception when you still try it (But it will contain corresponding config overrides if any are specified)
  • If you want to change the configuration you need to get it as "editable", so you can save it(But it will not contain any overrides)

Configuration API

But for config entities this is not described as clearly. The only information you get is that for "admin"- routes (/admin/*) if config entities are used as route parameters, their values are ensured to be not-overriden (like e.g. routes that use an entity form); but only on those routes.

Config enties actually do not distinguish between a "immutable" & "editable" variants.
This allows for the following two code flows:
a) Loading an not-overriden version of the config entity, change it, save it:

  • load/change: the entity contains not-overriden, possibly changed, values
  • preSave: the not-overriden values can be compared agains the "original" version (the "original" does contain overriden values)
  • save: the not-overriden values may be cast to match the configuration schema & are stored as configuration
  • postSave: the not-overriden values (which may have been cast to match the schema) can be compared agains the "original" version (the "original" does contain overriden values)

b) Loading an overriden version of the config entity, change it, save it

  • load/change: the entity contains overriden (possibly changed) values (the changed values might be the ones that were actually overriden or not)
  • preSave: the overriden values can be compared agains the "original" version (the "original" does contain overriden values)
  • save: the overriden values may be cast to match the configuration schema & are stored as configuration (At this point overriden values might be stored although they were not actively modified)
  • postSave: the overriden values (which may have been cast to match the schema) can be compared agains the "original" version (the "original" does contain overriden values)

This shows on one hand that as @druken monkey described above the code in the preSave/postSave hooks/entity methods may be called with overriden or not overriden values. On the other hand i fear it shows a bug that allows overriden values to accidentaly be leaked into the actual "editable" configuration.

proposed Solution

The solution for this two problems i think is:

  • Introduce a flag to config entities like: isEditable / isImmutable / mayContainOverrides
  • Per default load config entities as: !isEditable / isImmutable / mayContainOverrides
  • Disallow saving entities in the ConfigEntity storage (by throwing an exception) which are: !isEditable / isImmutable / mayContainOverrides
  • Maybe add a method on config entities / simply use the ConfigEntityStorage::loadOverrideFree to get instances that are flagged as: isEditable / !isImmutable / !mayContainOverrides
  • Since all config saved would now be ensured as not-overriden, preSave hooks/entity methods should get an not-overriden original
  • postSave hooks/entity methods would also get a not-overriden original
  • If someone needs an overriden original in preSave, they could simply load an overriden version (as the changes are not yet persisted)
  • If someone needs an overriden original in a postSave hook / method, they could could theoretically set it as a seperate Field like maybe 'overridenOriginal' in a corresponding preSave hook/ method so they cant it get's passed along to postSave hook/ method
  • A generic 'overrideOriginal' solution would also be possible but would need to be implemented at the storage level (the "original" should be unset at the end, but because entity->postSave ist called before the entity hooks this could not be done in the config entity class)

This way the beghaviour would be more in line with the underlying configuration API & And i thing preSave/postSave hooks/entity methods work with a more predictable context.

Alumei’s picture

Status: Active » Needs review
StatusFileSize
new5.18 KB

I made this patch to prove my point.

I think this shows that the described inconsistencies are not only unpleasant but also point out a problematic bug.

Status: Needs review » Needs work

The last submitted patch, 3: test-to-prove-overide-problem--2744057-3.patch, failed testing.

Alumei’s picture

Now I think fixing this bug in the way proposed in #2 will actually also solve the inconstistency problems.
That's because per default the preSave/postSave hooks/entity methods would only be called on the 'not-overriden' values & therefore as pointed out before behave more predictable.

Alumei’s picture

I tried to implement the solution incremantally in 5 different ways as I am unsure what is acceptable within the existing BC policy:

  • Version A: Directly modify ConfigEnityBase & ConfigEntityStorage (Existing code may get an exception thrown at unecpected times)
  • Version B: Directly modify ConfigEnityBase & ConfigEntityStorage; Only thrown an exception if the corresponding EnityType has set a flag in it's annotation for supporting config overrides
  • Version C: Directly modify ConfigEnityBase & ConfigEntityStorage + Add OverrideSupportingConfigEntityInterface; Only thrown an exception if the corresponding EnityType implements that interface
  • Version D: Directly modify ConfigEntityStorage + Add OverrideSupportingConfigEntityInterface & OverrideSupportingConfigEntityBase; Only thrown an exception if the corresponding EnityType implements that interface
  • Version E: Add OverrideSupportingConfigEntityInterface, OverrideSupportingConfigEntityBase & OverrideSupportingConfigEntityStorage; Only thrown an exception if the corresponding EnityType implements that interface. Direct save() calls on the storage are also only secure if the correct storage base class is used

The last submitted patch, 6: A-immutable-config-override-support--2744057-6.patch, failed testing.

The last submitted patch, 6: B-immutable-config-override-support--2744057-6.patch, failed testing.

The last submitted patch, 6: D-immutable-config-override-support--2744057-6.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 6: E-immutable-config-override-support--2744057-6.patch, failed testing.

The last submitted patch, 6: C-immutable-config-override-support--2744057-6.patch, failed testing.

Alumei’s picture

I tried to fix the problems in Version A and Version B (as repesentative for C,D & E) by improving the override detection.

The general idea is to set the respective immutability information when retrieving the configuration using \Drupal\Core\Config\Config.
The *2.. patches change the methods on \Drupal\Core\Config\Config to only add the mutability information when beeing retrieved for config entities.

Alumei’s picture

StatusFileSize
new1.88 KB

Wrong interdiff ...

The last submitted patch, 12: A-immutable-config-override-support--2744057-12.patch, failed testing.

The last submitted patch, 12: A2-immutable-config-override-support--2744057-12.patch, failed testing.

The last submitted patch, 12: B-immutable-config-override-support--2744057-12.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 12: B2-immutable-config-override-support--2744057-12.patch, failed testing.

Alumei’s picture

Status: Needs work » Needs review
StatusFileSize
new2.35 KB
new10.32 KB

Tried to fix the fails by relocating the immutability info assignment back to the storage & using a new method containsOverrides() on \Drupal\Core\Config\Config to accuratly assign it.

Status: Needs review » Needs work

The last submitted patch, 18: A-immutable-config-override-support--2744057-18.patch, failed testing.

Alumei’s picture

Status: Needs work » Needs review
StatusFileSize
new10.35 KB
new810 bytes

The last submitted patch, 20: A-immutable-config-override-support--2744057-20.patch, failed testing.

Alumei’s picture

Alumei’s picture

StatusFileSize
new3.41 KB
new11.86 KB

Status: Needs review » Needs work

The last submitted patch, 23: B-immutable-config-override-support--2744057-23.patch, failed testing.

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.0-beta1 was released on August 3, 2016, which means new developments and disruptive changes should now be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

The last submitted patch, 23: B-immutable-config-override-support--2744057-23.patch, failed testing.

Alumei’s picture

I had some time to think about it more and came to the following conclusion:
This problem only affects the active usage of config overrides which in turn seems to be an edge-case in and of it self. There fore it is propably best to go with an opt-in approach.

With the attached patch core would provide a special interface OverrideSupportingConfigEntityInterface, a corresponding base class OverrideSupportingConfigEntityBase & strorage class OverrideSupportingConfigEntityStorage for config entities extending their existing counterparts.
Both OverrideSupportingConfigEntityBase & OverrideSupportingConfigEntityStorage contain checks that prevent overriden config values from beeing saved via config entities.

What do you think?

Alumei’s picture

Issue summary: View changes

Updated issue summary!

The previous patch is ment as a foundation, but what to do with it ...

If we would go with it, it would be a non-breaking api addition (i believe). Still the issue would remain that users of config overrides could have those overrides leak unexpectedly.
I see 5+ possible solutions for that:

  1. Change nothing
  2. Mark the old config entity classes as deprecated & point to these new classes
  3. Warn the users when when using old entity classes & an leakage might have occured (watchdog log / drupal message). Possible combine with (2)
  4. Try to prevent overrides from beeing saved (notify when it happens). Possibly combine with (3) and or (2)
  5. Any of the other ideas proposed in #2744057-6: Inconsistencies when updating overridden config entities
Alumei’s picture

Issue summary: View changes

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.0-alpha1 will be released the week of January 30, 2017, which means new developments and disruptive changes should now be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

joegl’s picture

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

geek-merlin’s picture

Ran into this.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new16.45 KB

The Needs Review Queue Bot tested this issue. It either no longer applies to Drupal core, or fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".

Apart from a re-roll or rebase, this issue may need more work to address feedback in the issue or MR comments. To progress an issue, incorporate this feedback as part of the process of updating the issue. This helps other contributors to know what is outstanding.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

drunken monkey’s picture

Linking #2910353: Prevent saving config entities when configuration overrides are applied, which I think would solve the same problem in a more radical, but therefore probably simpler way: By preventing overridden config entities from being updated at all.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.