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

Code before:
    def _resolve_config_credentials(
        cls,
        config: dict[str, dict[str, Any]] | None,
        credentials: dict[str, dict[str, Any]] | None,
    ) -> dict[str, dict[str, Any]]:
        """Initialize the dataset configuration with resolved credentials."""
        config = config or {}
        credentials = credentials or {}
        resolved_configs = {}

        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)


Review comment (security):
I see `unresolve_config_credentials` is public and `_resolve_config_credentials` is private, and I'm guessing that's because of how it's used in the KedroDataCatalog. Did you consider making unresolved config a property as well so it's accessed in the same way as the resolved config?

Code after:
    def resolve_credentials(
        cls,
        config: dict[str, dict[str, Any]] | None,
        credentials: dict[str, dict[str, Any]] | None,
    ) -> dict[str, dict[str, Any]]:
        """Initialize the dataset configuration with resolved credentials."""
        config = config or {}
        credentials = credentials or {}
        resolved_configs = {}

        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}"