Spaces:
Build error
Build error
Download config/credentials.py from abinazebinoy/bharatgraph: direct link, hf CLI and curl.
- Browser
- Download file 8.14 kB
-
https://huggingface.co/spaces/abinazebinoy/bharatgraph/resolve/main/config/credentials.py
- Command line
-
hf download hf://spaces/abinazebinoy/bharatgraph/config/credentials.py
-
curl -L -o credentials.py https://huggingface.co/spaces/abinazebinoy/bharatgraph/resolve/main/config/credentials.py
8.14 kB
| """ | |
| Phase D6 -- OS-keychain-backed credential storage. | |
| Extends config.settings's existing 2-level precedence cascade (real env | |
| var > local config.json) with a third, LOWEST-precedence source: the OS | |
| keychain (Windows Credential Manager / macOS Keychain / Linux Secret | |
| Service), accessed via the `keyring` package. | |
| Mirrors config.settings's established "inject into os.environ" design | |
| exactly, so no other call site in the codebase (every existing | |
| os.getenv(...) call) needs to change -- this module is only ever | |
| consumed via load_credentials_into_environ(), called once from | |
| config/settings.py, after the local-config injection loop and before | |
| NEO4J_URI etc. are read as module-level constants. | |
| A closed, explicit list of 6 managed keys -- deliberately not "whatever | |
| key someone passes in" -- because the OS keychain is a shared, per-user | |
| credential store; writing arbitrary app data into it would misuse the | |
| mechanism. | |
| Every keyring call is wrapped in try/except at the point of *use* | |
| (get_password/set_password), not at import or backend-selection time, | |
| because that is empirically where a missing backend actually raises | |
| (NoKeyringError): keyring itself does not raise on `import keyring` or | |
| `keyring.get_keyring()` even with zero usable backends -- it silently | |
| falls back to a no-op "fail" backend and only errors when something | |
| actually tries to read or write through it. | |
| """ | |
| import os | |
| try: | |
| import keyring | |
| except ImportError: # pragma: no cover -- keyring is a requirements.txt | |
| # dependency now, but importing this module must never crash a | |
| # process that somehow lacks it (e.g. an old frozen build made | |
| # before keyring was added) -- degrade to "no keychain available." | |
| keyring = None | |
| _SERVICE_NAME = "BharatGraph" | |
| # Closed list -- the ONLY keys this module will ever read from or write | |
| # to the OS keychain. Anything else is a programming error, not a | |
| # missing-value case. | |
| MANAGED_KEYS = frozenset({ | |
| "ADMIN_SECRET", | |
| "NEO4J_URI", | |
| "NEO4J_USER", | |
| "NEO4J_PASSWORD", | |
| "DATAGOV_API_KEY", | |
| "OPENSANCTIONS_API_KEY", | |
| }) | |
| def _check_managed(key: str) -> None: | |
| assert key in MANAGED_KEYS, ( | |
| f"{key!r} is not one of config.credentials.MANAGED_KEYS -- the " | |
| "OS keychain is a shared, per-user store and this module " | |
| "deliberately only manages a fixed, explicit set of keys." | |
| ) | |
| def get_credential(key: str) -> str: | |
| """Read one managed credential from the OS keychain. | |
| Returns "" (never raises) if keyring isn't importable, no backend | |
| is available, or the key was never set -- callers throughout this | |
| codebase already treat an empty string as "not configured" (every | |
| os.getenv(key, "") call site), so this matches that convention | |
| instead of introducing a new one. | |
| """ | |
| _check_managed(key) | |
| if keyring is None: | |
| return "" | |
| try: | |
| value = keyring.get_password(_SERVICE_NAME, key) | |
| except Exception: | |
| # Broad on purpose: a missing backend raises NoKeyringError, but | |
| # a *working* backend can still raise its own backend-specific | |
| # error (e.g. a locked Secret Service prompt the user dismissed). | |
| # Both mean "treat as unset" here. | |
| return "" | |
| return value or "" | |
| def set_credential(key: str, value: str) -> bool: | |
| """Write one managed credential to the OS keychain. | |
| Returns True on success, False on any failure (no keyring, no | |
| backend, backend-specific error) -- never raises. Callers (today, | |
| only api/routes/admin.py's auto-generated ADMIN_SECRET) fall back to | |
| the legacy config.json mechanism when this returns False. | |
| """ | |
| _check_managed(key) | |
| if keyring is None: | |
| return False | |
| try: | |
| keyring.set_password(_SERVICE_NAME, key, value) | |
| return True | |
| except Exception: | |
| return False | |
| def load_credentials_into_environ() -> None: | |
| """Inject every managed credential into os.environ, at the lowest | |
| precedence: a value already present in os.environ -- whether from a | |
| real environment variable, .env, or config.settings's own local | |
| config.json injection, which runs before this is called -- is never | |
| overwritten. | |
| Safe to call unconditionally, including in web mode: with no OS | |
| keychain backend available, get_credential() returns "" for every | |
| key and this becomes a no-op. | |
| """ | |
| for key in MANAGED_KEYS: | |
| if os.getenv(key): | |
| continue | |
| value = get_credential(key) | |
| if value: | |
| os.environ[key] = value | |
| def _mask(value: str) -> str: | |
| """Mask a secret for display: show at most its last 4 characters.""" | |
| if len(value) <= 4: | |
| return "*" * len(value) | |
| return "*" * (len(value) - 4) + value[-4:] | |
| def _cli() -> int: | |
| """Tiny CLI for Phase D6, ahead of D5's setup-wizard UI. | |
| Usage: | |
| python -m config.credentials list | |
| python -m config.credentials get KEY [--reveal] | |
| python -m config.credentials set KEY [VALUE] | |
| python -m config.credentials delete KEY | |
| Exists so the 5 managed keys with no other write path yet (every- | |
| thing except ADMIN_SECRET, which api/routes/admin.py already writes | |
| automatically) can be tested against a REAL OS keychain -- something | |
| no amount of sandbox simulation can substitute for -- before D5's | |
| setup wizard exists to do this through an actual UI. Not a | |
| replacement for D5: set_credential()/get_credential() are the exact | |
| same functions a future UI would call, so nothing here needs to | |
| change when that UI is built. | |
| """ | |
| import argparse | |
| import getpass | |
| parser = argparse.ArgumentParser( | |
| prog="python -m config.credentials", | |
| description="Manage BharatGraph's OS-keychain-stored credentials (Phase D6).", | |
| ) | |
| sub = parser.add_subparsers(dest="command", required=True) | |
| sub.add_parser("list", help="Show which managed keys currently have a value (masked).") | |
| p_get = sub.add_parser("get", help="Read one managed key.") | |
| p_get.add_argument("key", choices=sorted(MANAGED_KEYS)) | |
| p_get.add_argument("--reveal", action="store_true", help="Print the full value instead of masking it.") | |
| p_set = sub.add_parser("set", help="Write one managed key.") | |
| p_set.add_argument("key", choices=sorted(MANAGED_KEYS)) | |
| p_set.add_argument("value", nargs="?", help="Value to store. Omit to be prompted (hidden input, not echoed).") | |
| p_del = sub.add_parser("delete", help="Remove one managed key from the keychain.") | |
| p_del.add_argument("key", choices=sorted(MANAGED_KEYS)) | |
| args = parser.parse_args() | |
| if keyring is None: | |
| print("keyring is not installed -- nothing to do.") | |
| return 1 | |
| print(f"Using keychain backend: {keyring.get_keyring()}") | |
| if args.command == "list": | |
| for key in sorted(MANAGED_KEYS): | |
| value = get_credential(key) | |
| print(f" {key:24s} {_mask(value) if value else '(not set)'}") | |
| return 0 | |
| if args.command == "get": | |
| value = get_credential(args.key) | |
| if not value: | |
| print(f"{args.key}: (not set)") | |
| return 1 | |
| print(f"{args.key}: {value if args.reveal else _mask(value)}") | |
| return 0 | |
| if args.command == "set": | |
| value = args.value if args.value is not None else getpass.getpass(f"Value for {args.key}: ") | |
| if not value: | |
| print("Refusing to store an empty value.") | |
| return 1 | |
| if set_credential(args.key, value): | |
| print(f"{args.key}: stored in OS keychain.") | |
| return 0 | |
| print(f"{args.key}: could not store in OS keychain (no backend available?). Nothing was written.") | |
| return 1 | |
| if args.command == "delete": | |
| try: | |
| keyring.delete_password(_SERVICE_NAME, args.key) | |
| print(f"{args.key}: deleted from OS keychain.") | |
| return 0 | |
| except Exception as e: | |
| print(f"{args.key}: could not delete ({type(e).__name__}: {e}).") | |
| return 1 | |
| return 1 # pragma: no cover -- argparse's required=True makes this unreachable | |
| if __name__ == "__main__": | |
| import sys as _sys | |
| _sys.exit(_cli()) | |