Repository navigation
Conversation
SecretValue keeps a secret out of logs, repr, str, f-strings, tracebacks, pickles and Pydantic/JSON output. The raw value is only available through unwrap(). - port of the SecretValue from closed PR nitrocloudofficial#4 with the snake_case API only - pickling redacts: a pickled secret loads back with no value and unwrap() raises - Pydantic v2 fields validate a SecretValue or str and serialize as the redacted marker - equality uses hmac.compare_digest - add is_secret_value() and unwrap_secret() - JWTModule(secret=...), ApiKeyModule(keys=[...]), OAuthService client secrets and ConfigService defaults/get_secret() accept a SecretValue; plain str still works
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #69
What this adds
SecretValue(nitrostack/auth/secret.py), exported fromnitrostack. It keeps a secret out of logs,repr, tracebacks, pickles and serialized output. The raw value is only reachable throughunwrap().Ported from closed PR #4, with the snake_case API only (no camelCase aliases).
from_env(name),from_env_optional(name),from_value(value, allow_hardcoded=..., reason=...),unwrap(),is_from_environment().str,repr,format, f-strings and logging all give[SecretValue: REDACTED]. There is no instance__dict__.unwrap()raises. I chose this over loading back a fake string, which a JWT or API key check could silently use as the real secret.SecretValueaccepts aSecretValueor astr, andmodel_dump(),model_dump_json()and the JSON schema never contain the value. A barejson.dumps(secret)raisesTypeErrorinstead of leaking.==useshmac.compare_digest.__hash__is keyed per process, so it does not expose the value.copyanddeepcopykeep the value.is_secret_value(x)andunwrap_secret(x)(a plainstris returned as it is).Modules
All of these still accept a plain
str.JWTModule.for_root(secret=...)/JWTService(secret=...): new optional explicit secret. It wins over the environment variable; with none given, behaviour is unchanged.ApiKeyModule.for_root(keys=[...])/ApiKeyService(keys=[...]): new optional explicit keys, used in addition to the environment ones.OAuthService/OAuthModule.for_root:token_introspection_client_secretandstatic_client_secretaccept aSecretValue. It is unwrapped only where it is really used: the Basic auth header for introspection and the RFC 7591 registration response.ConfigService:defaultsmay holdSecretValuevalues. Newget_secret()andget_secret_or_throw()return aSecretValue.Note:
JWTModuleandApiKeyModuletook environment variable names before, not secret strings, so the newsecretandkeysarguments are how a secret reaches them in code.Differences from the old PR #4 code
requiredargument offrom_env(it never changed the behaviour; usefrom_env_optionalinstead).warnings.warninstead of writing to stderr based onNODE_ENV.unwrap_secretno longer prints a warning for plain strings, because the modules must keep accepting them.repris now fully redacted (the old one printed the source).Checks
tests/test_secret_value.py: 40 tests covering each acceptance criterion (f-string,repr,model_dump_json,json.dumps(model.model_dump()),pickle.dumpsat every protocol, log records, tracebacks with captured locals) and each module.repr, a pickle that keeps the value, a Pydantic serializer that unwraps, and a plain==). Each was caught by at least one test.develop.Not changed
JWTService.get_secret()still falls back to a built-in development secret when nothing is configured. That is existing behaviour and out of scope here, but it is probably worth its own issue.