Load and validate ``auth.json``, returning configured entries. Returns an empty list when the file does not exist — this means all HTTP requests will be unauthenticated (opt-in model). Raises ``ValueError`` on schema violations. Callers that want misconfigurations to fail fast can
(
path: Path | None = None,
)
| 54 | |
| 55 | |
| 56 | def load_auth_config( |
| 57 | path: Path | None = None, |
| 58 | ) -> list[AuthConfigEntry]: |
| 59 | """Load and validate ``auth.json``, returning configured entries. |
| 60 | |
| 61 | Returns an empty list when the file does not exist — this means |
| 62 | all HTTP requests will be unauthenticated (opt-in model). |
| 63 | |
| 64 | Raises ``ValueError`` on schema violations. Callers that want |
| 65 | misconfigurations to fail fast can allow this exception to |
| 66 | propagate; higher-level HTTP helpers may instead catch it, |
| 67 | warn, and continue with unauthenticated requests. |
| 68 | """ |
| 69 | config_path = path or _default_config_path() |
| 70 | |
| 71 | if not config_path.is_file(): |
| 72 | return [] |
| 73 | |
| 74 | # Warn (but don't fail) if the file is world-readable (POSIX only). |
| 75 | if os.name != "nt": |
| 76 | try: |
| 77 | mode = config_path.stat().st_mode |
| 78 | if mode & (stat.S_IRGRP | stat.S_IROTH): |
| 79 | import warnings |
| 80 | |
| 81 | warnings.warn( |
| 82 | f"{config_path} is readable by group/others. " |
| 83 | "Consider restricting with: chmod 600 " |
| 84 | f"{config_path}", |
| 85 | UserWarning, |
| 86 | stacklevel=2, |
| 87 | ) |
| 88 | except OSError: |
| 89 | pass # stat failed — skip permission check |
| 90 | |
| 91 | raw = json.loads(config_path.read_text(encoding="utf-8")) |
| 92 | |
| 93 | if not isinstance(raw, dict): |
| 94 | raise ValueError(f"auth.json must be a JSON object, got {type(raw).__name__}") |
| 95 | |
| 96 | providers_raw = raw.get("providers") |
| 97 | if not isinstance(providers_raw, list): |
| 98 | raise ValueError("auth.json must contain a 'providers' array") |
| 99 | |
| 100 | entries: list[AuthConfigEntry] = [] |
| 101 | for i, entry_raw in enumerate(providers_raw): |
| 102 | if not isinstance(entry_raw, dict): |
| 103 | raise ValueError(f"providers[{i}]: must be a JSON object") |
| 104 | |
| 105 | hosts = entry_raw.get("hosts") |
| 106 | if not isinstance(hosts, list) or not hosts: |
| 107 | raise ValueError(f"providers[{i}]: 'hosts' must be a non-empty array") |
| 108 | if not all(isinstance(h, str) and h.strip() for h in hosts): |
| 109 | raise ValueError(f"providers[{i}]: each host must be a non-empty string") |
| 110 | # Normalize hosts: strip whitespace and lowercase |
| 111 | hosts = [h.strip().lower() for h in hosts] |
| 112 | # Reject dangerous wildcard forms (e.g. *github.com matches github.com.evil.com) |
| 113 | for h in hosts: |