Repository: kedro-org/kedro
File: kedro/io/catalog_config_resolver.py (Python)

Code before:

        for ds_name, ds_config in config.items():
            if not isinstance(ds_config, dict):
                raise DatasetError(
                    f"Catalog entry '{ds_name}' is not a valid dataset configuration. "
                    "\nHint: If this catalog entry is intended for variable interpolation, "
                    "make sure that the key is preceded by an underscore."
                )
            if not cls.is_pattern(ds_name):
                resolved_configs[ds_name] = cls._resolve_credentials(
                    ds_config, credentials
                )

        return resolved_configs

    @classmethod
    def unresolve_config_credentials(
        cls, cred_name: str, ds_config: dict[str, dict[str, Any]] | None
    ) -> tuple[dict[str, dict[str, Any]], dict[str, dict[str, Any]]]:
        ds_config_copy = copy.deepcopy(ds_config) or {}
        credentials: dict[str, Any] = {}
        credentials_ref = f"{cred_name}_{CREDENTIALS_KEY}"

        def unresolve(config: Any) -> None:
            # We don't expect credentials key appears more than once in the config,
            # So once we found the key first time we unresolve it and stop iterating after
            for key, val in config.items():
                if key == CREDENTIALS_KEY and config[key]:
                    credentials[credentials_ref] = config[key]
                    config[key] = credentials_ref
                    return
                if isinstance(val, dict):
                    unresolve(val)

        unresolve(ds_config_copy)

        return ds_config_copy, credentials

    def resolve_pattern(self, ds_name: str) -> dict[str, Any]:
        """Resolve dataset patterns and return resolved configurations based on the existing patterns."""
        matched_pattern = self.match_pattern(ds_name)

        if matched_pattern and ds_name not in self._resolved_configs:
            pattern_config = self._get_pattern_config(matched_pattern)
            ds_config = self._resolve_dataset_config(
                ds_name, matched_pattern, copy.deepcopy(pattern_config)
            )

            if (
                self._pattern_specificity(matched_pattern) == 0
                and matched_pattern in self._default_pattern

Review comment (security):
So to clarify: we expect that there's only _one_ `credentials.yml` file across the base and overriding environment, correct? That makes total sense, but I didn't immediately get that from the note here. "credentials key" sounds to me like the actual key used on a catalog entry, like in:

```
motorbikes:
  type: pandas.CSVDataset
  filepath: s3://your_bucket/data/02_intermediate/company/motorbikes.csv
  credentials: dev_s3
  load_args:
    sep: ','
    skiprows: 5
    skipfooter: 1
    na_values: ['#NA', NA]
```

Code after:

        for ds_name, ds_config in config.items():
            if not isinstance(ds_config, dict):
                raise DatasetError(
                    f"Catalog entry '{ds_name}' is not a valid dataset configuration. "
                    "\nHint: If this catalog entry is intended for variable interpolation, "
                    "make sure that the key is preceded by an underscore."
                )
            if not cls.is_pattern(ds_name):
                resolved_configs[ds_name] = cls._resolve_credentials(
                    ds_config, credentials
                )

        return resolved_configs

    @staticmethod
    def unresolve_credentials(
        cred_name: str, ds_config: dict[str, dict[str, Any]] | None
    ) -> tuple[dict[str, dict[str, Any]], dict[str, dict[str, Any]]]:
        """Extracts and replaces credentials in a dataset configuration with
        references, ensuring separation of credentials from the dataset configuration.

        Credentials are searched for recursively in the dataset configuration.
        The first occurrence of the `CREDENTIALS_KEY` is replaced with a generated
        reference key.

        Args:
            cred_name: A unique identifier for the credentials being unresolved.
                This is used to generate a reference key for the credentials.
            ds_config: The dataset configuration containing potential credentials
                under the key `CREDENTIALS_KEY`.

        Returns:
            A tuple containing:
                ds_config_copy : A deep copy of the original dataset
                    configuration with credentials replaced by reference keys.
                credentials: A dictionary mapping generated reference keys to the original credentials.
        """
        ds_config_copy = copy.deepcopy(ds_config) or {}
        credentials: dict[str, Any] = {}
        credentials_ref = f"{cred_name}_{CREDENTIALS_KEY}"

        def unresolve(config: Any) -> None:
            # We don't expect credentials key appears more than once within the same dataset config,
            # So once we found the key first time we unresolve it and stop iterating after
            for key, val in config.items():
                if key == CREDENTIALS_KEY and config[key]:
                    credentials[credentials_ref] = config[key]
                    config[key] = credentials_ref
                    return
                if isinstance(val, dict):