Manages CodeWiki configuration with secure keyring storage for API keys. Storage: - API key: System keychain via keyring (macOS Keychain, Windows Credential Manager, Linux Secret Service) - Fallback: ~/.codewiki/credentials.json when keyring is unavailable
| 32 | |
| 33 | |
| 34 | class ConfigManager: |
| 35 | """ |
| 36 | Manages CodeWiki configuration with secure keyring storage for API keys. |
| 37 | |
| 38 | Storage: |
| 39 | - API key: System keychain via keyring (macOS Keychain, Windows Credential Manager, |
| 40 | Linux Secret Service) |
| 41 | - Fallback: ~/.codewiki/credentials.json when keyring is unavailable |
| 42 | - Other settings: ~/.codewiki/config.json |
| 43 | |
| 44 | Set CODEWIKI_NO_KEYRING=1 to skip keyring and use file-based storage. |
| 45 | """ |
| 46 | |
| 47 | def __init__(self): |
| 48 | """Initialize the configuration manager.""" |
| 49 | self._api_key: Optional[str] = None |
| 50 | self._config: Optional[Configuration] = None |
| 51 | self._force_no_keyring = os.environ.get("CODEWIKI_NO_KEYRING", "").strip() in ( |
| 52 | "1", |
| 53 | "true", |
| 54 | "yes", |
| 55 | ) |
| 56 | self._keyring_available = self._check_keyring_available() |
| 57 | |
| 58 | def _check_keyring_available(self) -> bool: |
| 59 | """Check if system keyring is available.""" |
| 60 | if self._force_no_keyring: |
| 61 | logger.debug("Keyring disabled via CODEWIKI_NO_KEYRING") |
| 62 | return False |
| 63 | try: |
| 64 | # Try to get/set a test value |
| 65 | keyring.get_password(KEYRING_SERVICE, "__test__") |
| 66 | return True |
| 67 | except (KeyringError, Exception): |
| 68 | return False |
| 69 | |
| 70 | def _load_api_key_from_file(self) -> Optional[str]: |
| 71 | """Load API key from fallback credentials file.""" |
| 72 | if not CREDENTIALS_FILE.exists(): |
| 73 | return None |
| 74 | try: |
| 75 | content = safe_read(CREDENTIALS_FILE) |
| 76 | data = json.loads(content) |
| 77 | return data.get("api_key") |
| 78 | except (json.JSONDecodeError, FileSystemError): |
| 79 | return None |
| 80 | |
| 81 | def _save_api_key_to_file(self, api_key: str): |
| 82 | """Save API key to fallback credentials file (plaintext).""" |
| 83 | ensure_directory(CONFIG_DIR) |
| 84 | data = {"api_key": api_key} |
| 85 | safe_write(CREDENTIALS_FILE, json.dumps(data, indent=2)) |
| 86 | # Restrict file permissions (owner read/write only) |
| 87 | try: |
| 88 | CREDENTIALS_FILE.chmod(0o600) |
| 89 | except OSError: |
| 90 | pass |
| 91 |
no outgoing calls
no test coverage detected