Problem/Motivation

#3324150: Add validation constraints to config_entity.dependencies introduced the ValidKeys constraint for use on type: mapping. This validation constraint has particularly strong test coverage (see \Drupal\KernelTests\Core\TypedData\ValidKeysConstraintValidatorTest).

It allows either specifying an explicit list of allowed keys, or inferring them based on the keys defined for a mapping in the config schema.

It is currently adopted by:

  1. config_dependencies_base → automatically inferred: ValidKeys: '<infer>'
  2. config_dependencies → automatically inferred: ValidKeys: '<infer>'
  3. _core_config_info → NOT automatically inferred: ValidKeys: ['default_config_hash'] (because this is a low-level config schema type that is NOT extensible, although it absolutely could use ValidKeys: '<infer>' too)

It made sense to introduce this constraint first and adopt it narrowly, and to then adopt it more widely. That's what this issue is about.

The whole point of type: mapping is that you have known keys. If you don't know, you have to use type: sequence. But there is no validation happening on this today 🙈.

Only the config_test.validation mapping is validated, for testing purposes, in \Drupal\KernelTests\Config\TypedConfigTest::testSimpleConfigValidation(), by \Drupal\config_test\ConfigValidation::validateMapping(). That is essentially a hardcoded equivalent of the ValidKeys validation constraint (introduced in #1928868: Typed config incorrectly implements Typed Data interfaces and last refined in #2925689: ConfigValidation class contains code that is brittle and changing for every addition).

Steps to reproduce

N/A

Proposed resolution

Use this constraint by default for type: mapping:

mapping:
    label: Mapping
    class: '\Drupal\Core\Config\Schema\Mapping'
    definition_class: '\Drupal\Core\TypedData\MapDataDefinition'
+   constraints:
+     # By default, only allow the explicitly listed mapping keys.
+     ValidKeys: '<infer>'

This has zero consequences today because no configuration is validated. (It's only checked if it matches the structure of the config schema, but only fairly loosely.)

The comment captures what it does. It makes type: mapping all across Drupal core actually sanely validatable by default! 🚀

Remaining tasks

TBD

User interface changes

None.

API changes

None.

Data model changes

type: mapping's implementation now actually matches its documentation: only known keys are allowed.

Release notes snippet

N/A

Issue fork drupal-3376794

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

Wim Leers created an issue. See original summary.

wim leers’s picture

wim leers’s picture

Title: type: mapping should use `ValidKeys: <infer>` constraint by default » `type: mapping` should use `ValidKeys: <infer>` constraint by default
Assigned: wim leers » Unassigned
Status: Active » Needs review

At this point, we could choose to remove the test-only validation constraint:

diff --git a/core/modules/config/tests/config_test/config/schema/config_test.schema.yml b/core/modules/config/tests/config_test/config/schema/config_test.schema.yml
index 2a707facb0..87f84bb9e3 100644
--- a/core/modules/config/tests/config_test/config/schema/config_test.schema.yml
+++ b/core/modules/config/tests/config_test/config/schema/config_test.schema.yml
@@ -172,9 +172,6 @@ system.action.*.third_party.config_test:
 config_test.validation:
   type: config_object
   label: 'Configuration type'
-  constraints:
-    Callback:
-      callback: [\Drupal\config_test\ConfigValidation, validateMapping]
   mapping:
     llama:
       type: string

… because it literally is surfacing the same problem that ValidKeys: '<infer>' surfaces, but with more precision. Applying the above diff would result in

    // 2 constraint violations triggered by the default validation constraint
    // for `type: mapping`
    // @see \Drupal\Core\Validation\Plugin\Validation\Constraint\ValidKeysConstraint
    $this->assertSame('', $result->get(0)->getPropertyPath());
    $this->assertEquals("'elephant' is not a supported key.", $result->get(0)->getMessage());
    $this->assertSame('', $result->get(1)->getPropertyPath());
    $this->assertEquals("'zebra' is not a supported key.", $result->get(1)->getMessage());

keeping this 👆 and removing this 👇

    // 1 additional constraint violation triggered by the custom
    // constraint for the `config_test.validation` type, which indirectly
    // extends `type: mapping` (via `type: config_object`).
    // @see \Drupal\config_test\ConfigValidation::validateMapping()
    $this->assertEquals('', $result->get(2)->getPropertyPath());
    $this->assertEquals('Unexpected keys: elephant, zebra', $result->get(2)->getMessage());

However, it's harmless to keep both. And in a way, it serves as a nice example to show that it's always possible to add additional validation constraints. So I vote to keep it as-is.

wim leers’s picture

Thanks to \Drupal\Core\Config\TypedConfigManager::determineType() and ::getFallbackName(), e.g. type: field.storage_settings.[%parent.type] automatically resolves to field.storage_settings.boolean, but then falls back to field.storage_settings.* (because there's no specific settings for the boolean field type).

However, type: field.storage_settings.* does not specify a concrete mapping … and nor does type: mapping! That's what this was crashing on.

So either we need to re-create all of the inheritance/extension logic in \Drupal\Core\Validation\Plugin\Validation\Constraint\ValidKeysConstraint::inferKeys() … or we just rely on \Drupal\Core\Config\TypedConfigManager::getDefinitionWithReplacements() doing that for us like it has done for the past ~decade. Then all we need to is ensure that even the root type (type: mapping) has a list of allowed mapping keys specified, even if that list is empty. That way, it is nicely inherited down the chain (type: mappingtype: field.storage_settings.*type: field.storage_settings.boolean) for us 🤓

Then everything starts working! 👍

borisson_’s picture

In comment #4 Wim wasn't sure about keeping the ValidMapping constraint. It is a different implementation, so not completely the same and I agree that it is valuable to keep both. It is a good example to keep.

I'm not sure I completely understand the comment in #5, but I can see why setting the ValidKeys: '<infer>' on the root mapping type bubbles up, so that makes a lot of sense.

This is probably the simplest of all the config schema validation mapping issues that was committed over the last few weeks and it provides some additional strictness. It is weird to see nothing in core actually have an error against this, as opposed to some of the other issues, but the test coverage proves this works.

wim leers’s picture

It is weird to see nothing in core actually have an error against this, as opposed to some of the other issues, but the test coverage proves this works.

It's not that weird, because we're not validating all config in Drupal core yet. #3361534: KernelTestBase::$strictConfigSchema = TRUE and BrowserTestBase::$strictConfigSchema = TRUE do not actually strictly validate would change that, and would likely trigger additional failures.

Regarding #5: see the test failures for the commits prior to the last, they contain output like this:

assert(array_key_exists('mapping', $definition)) in /var/www/html/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ValidKeysConstraint.php:93

That means the mapping key-value pair was not defined on the given config schema type definition. So, for example, field.storage_settings.* does not contain it. And hence the assertion failed.

By adding mapping: {} to type: mapping's definition, we ensure that the mapping key-value pair is always defined for all mappings (and child types such as config_object, as well as grandchild types such as field.storage_settings.*). That's what #5 tried to explain, probably not concretely enough 🙈

borisson_’s picture

Status: Needs review » Reviewed & tested by the community
wim leers’s picture

🤣 Thanks… I guess? :P

borisson_’s picture

wim leers’s picture

Assigned: Unassigned » wim leers
Status: Reviewed & tested by the community » Needs work

Did that!

Tests are still running, but I already see several failures. Yay for #3361534: KernelTestBase::$strictConfigSchema = TRUE and BrowserTestBase::$strictConfigSchema = TRUE do not actually strictly validate keeping us more honest going forward! 😊

Will investigate the failures as soon as the full test suite finishes running.

wim leers’s picture

Assigned: wim leers » Unassigned
Status: Needs work » Needs review

Well that turned out to be very simple! 🤩 Barely any violations in Drupal core! 👍

borisson_’s picture

Status: Needs review » Reviewed & tested by the community

This is great, super happy to see that the strict config schema checking found 5 more fails.

  • longwave committed b90b2e30 on 11.x
    Issue #3376794 by Wim Leers, borisson_: `type: mapping` should use `...
longwave’s picture

Status: Reviewed & tested by the community » Fixed

Committed b90b2e3 and pushed to 11.x. Thanks!

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

wim leers’s picture