param icon indicating copy to clipboard operation
param copied to clipboard

Create a new Secret parameter or as keyword

Open ahuang11 opened this issue 9 months ago • 8 comments

Add support for secret parameter types, similar to Pydantic's SecretStr/SecretBytes implementation, to help protect sensitive information from being accidentally exposed in logs, debugging output, or string representations.

I imagine either having a dedicated param:

import param

class Config(param.Parameterized):
    api_key = param.Secret(default="sk_test_abcdef")
    
config = Config()
print(config.api_key)  # Would display: "Secret('**********')"
print(config.api_key.get_secret_value())  # Would return: "sk_test_abcdef"

Or, a secret kwarg on String

import param

class Config(param.Parameterized):
    api_key = param.String(default="sk_test_abcdef", secret=True)
    
config = Config()
print(config.api_key)  # Would display: "**********"
print(config.get_secret_value("api_key"))  # Would return: "sk_test_abcdef"

ahuang11 avatar Mar 18 '25 15:03 ahuang11

Why not just overwrite __str__ and __repr__ to return ***?

hoxbro avatar Mar 18 '25 15:03 hoxbro

Why not just overwrite __str__ and __repr__ to return ***?

Is that how the param.Secret parameter should be implemented?

MarcSkovMadsen avatar Mar 18 '25 16:03 MarcSkovMadsen

Interesting use case! Pydantic wraps the string/byte input in a custom class. I think that means this shouldn't be implemented with a secret kwarg on String, as the Parameter value will no longer be a str object. I am in fact not sure there's an existing Parameter that casts the input value like this one would do (str to custom Secret object). It doesn't mean we shouldn't do it, just that it might be a new pattern introduced in the code base.

Image

Why not just overwrite __str__ and __repr__ to return ***?

Yep that's not very clear. But if the implementation wraps the input string/byte in a new object then yes this object should define __str__/__repr__ to hide the secret in all the places it could show up (logs, error messages, etc.).

maximlt avatar Mar 18 '25 16:03 maximlt

This use case is related to what pickle_default_value (removed as described in https://github.com/holoviz/param/issues/1007 ) was meant for. In that case the Parameter value didn't need special behavior for displaying representations on screen, but it did need need special handling for state saving and for script_repr.

jbednar avatar Mar 18 '25 16:03 jbednar

Is that how the param.Secret parameter should be implemented?

The key is in memory, so as long as we don't print (and also don't dump it with serialize), I don't think there is much more we can do - I could be wrong!

Edit: A small example of how it could look like.

class SecretStr(str):
    def __str__(self):
        return "***"

    def __repr__(self):
        return "***"


class Secret(String):
    _slot_defaults = dict(Parameter._slot_defaults, default=SecretStr(), regex=None)

    def __init__(self, default=Undefined, *, regex=Undefined, **kwargs):
        if isinstance(default, str):
            default = SecretStr(default)
        super().__init__(default=default, **kwargs)

    def __set__(self, obj, val):
        if isinstance(val, str):
            val = SecretStr(val)
        return super().__set__(obj, val)

    @classmethod
    def serialize(cls, value):
        return "***"

    @classmethod
    def deserialize(cls, value):
        return "***"

    def _validate_value(self, val, allow_None):
        if allow_None and val is None:
            return
        if not isinstance(val, SecretStr):
            raise ValueError(
                f'{_validate_error_prefix(self)} only takes a string value, '
                f'not value of {type(val)}.'
            )

hoxbro avatar Mar 18 '25 17:03 hoxbro

The serialize/deserialize and repr implementations are part of this but I'd say the primary reason for param.Secret to exist would be semantic, i.e. downstream libraries will know that Secret strings should be treated differently. I'm not sure if wrapping the string type is desirable.

philippjfr avatar Mar 19 '25 18:03 philippjfr

Right. An example of such downstream handling is in https://github.com/holoviz/param/issues/1007, where Topographica knew not to serialize parameters with the indicated metadata. But in that case it was a Path parameter, not a String, and those two types don't share a parent class other than Parameterized. I guess my vote would be for param.Secret rather than a flag on String, since it will stand out more and be clearer that it needs special handling.

jbednar avatar Mar 19 '25 18:03 jbednar

Is the PasswordInput in pmui related to this? Does it achieve (part of) the goal that @Andrew describes at the top of this issue?

And if so, which existing Params in param is the component using? Or is it done in a different way?

Or is the purpose of the component purely to obfuscate the value during input, and it does not do anything special in terms of storing / marking it as secret?

https://panel-material-ui.holoviz.org/reference/widgets/PasswordInput.html

Coderambling avatar Jun 15 '25 20:06 Coderambling