PlanOpticon

Source Blame History 485 lines
0981a08… noreply 1 """Unified OAuth and authentication strategy for PlanOpticon connectors.
0981a08… noreply 2
0981a08… noreply 3 Provides a consistent auth pattern across all source connectors:
0981a08… noreply 4 1. Saved token (auto-refresh if expired)
0981a08… noreply 5 2. OAuth 2.0 (Authorization Code with PKCE, or Client Credentials)
0981a08… noreply 6 3. API key fallback (environment variable)
0981a08… noreply 7
0981a08… noreply 8 Usage in a connector:
0981a08… noreply 9
0981a08… noreply 10 from video_processor.auth import OAuthManager, AuthConfig
0981a08… noreply 11
0981a08… noreply 12 config = AuthConfig(
0981a08… noreply 13 service="notion",
0981a08… noreply 14 oauth_authorize_url="https://api.notion.com/v1/oauth/authorize",
0981a08… noreply 15 oauth_token_url="https://api.notion.com/v1/oauth/token",
0981a08… noreply 16 client_id_env="NOTION_CLIENT_ID",
0981a08… noreply 17 client_secret_env="NOTION_CLIENT_SECRET",
0981a08… noreply 18 api_key_env="NOTION_API_KEY",
0981a08… noreply 19 scopes=["read_content"],
0981a08… noreply 20 )
0981a08… noreply 21 manager = OAuthManager(config)
0981a08… noreply 22 token = manager.authenticate() # Returns access token or None
0981a08… noreply 23 """
0981a08… noreply 24
0981a08… noreply 25 import base64
0981a08… noreply 26 import hashlib
0981a08… noreply 27 import json
0981a08… noreply 28 import logging
0981a08… noreply 29 import os
0981a08… noreply 30 import secrets
0981a08… noreply 31 import time
0981a08… noreply 32 import webbrowser
0981a08… noreply 33 from dataclasses import dataclass, field
0981a08… noreply 34 from pathlib import Path
0981a08… noreply 35 from typing import Dict, List, Optional
0981a08… noreply 36
0981a08… noreply 37 logger = logging.getLogger(__name__)
0981a08… noreply 38
0981a08… noreply 39 TOKEN_DIR = Path.home() / ".planopticon"
0981a08… noreply 40
0981a08… noreply 41
0981a08… noreply 42 @dataclass
0981a08… noreply 43 class AuthConfig:
0981a08… noreply 44 """Configuration for a service's authentication."""
0981a08… noreply 45
0981a08… noreply 46 service: str
0981a08… noreply 47
0981a08… noreply 48 # OAuth endpoints (set both for OAuth support)
0981a08… noreply 49 oauth_authorize_url: Optional[str] = None
0981a08… noreply 50 oauth_token_url: Optional[str] = None
0981a08… noreply 51
0981a08… noreply 52 # Client credentials (checked from env if not provided)
0981a08… noreply 53 client_id: Optional[str] = None
0981a08… noreply 54 client_secret: Optional[str] = None
0981a08… noreply 55 client_id_env: Optional[str] = None
0981a08… noreply 56 client_secret_env: Optional[str] = None
0981a08… noreply 57
0981a08… noreply 58 # API key fallback
0981a08… noreply 59 api_key_env: Optional[str] = None
0981a08… noreply 60
0981a08… noreply 61 # OAuth scopes
0981a08… noreply 62 scopes: List[str] = field(default_factory=list)
0981a08… noreply 63
0981a08… noreply 64 # Redirect URI for auth code flow
0981a08… noreply 65 redirect_uri: str = "urn:ietf:wg:oauth:2.0:oob"
0981a08… noreply 66
0981a08… noreply 67 # Server-to-Server (client credentials grant)
0981a08… noreply 68 account_id: Optional[str] = None
0981a08… noreply 69 account_id_env: Optional[str] = None
0981a08… noreply 70
0981a08… noreply 71 # Token storage
0981a08… noreply 72 token_path: Optional[Path] = None
0981a08… noreply 73
0981a08… noreply 74 @property
0981a08… noreply 75 def resolved_client_id(self) -> Optional[str]:
0981a08… noreply 76 return (
0981a08… noreply 77 self.client_id
0981a08… noreply 78 or (os.environ.get(self.client_id_env, "") if self.client_id_env else None)
0981a08… noreply 79 or None
0981a08… noreply 80 )
0981a08… noreply 81
0981a08… noreply 82 @property
0981a08… noreply 83 def resolved_client_secret(self) -> Optional[str]:
0981a08… noreply 84 return (
0981a08… noreply 85 self.client_secret
0981a08… noreply 86 or (os.environ.get(self.client_secret_env, "") if self.client_secret_env else None)
0981a08… noreply 87 or None
0981a08… noreply 88 )
0981a08… noreply 89
0981a08… noreply 90 @property
0981a08… noreply 91 def resolved_api_key(self) -> Optional[str]:
0981a08… noreply 92 if self.api_key_env:
0981a08… noreply 93 val = os.environ.get(self.api_key_env, "")
0981a08… noreply 94 return val if val else None
0981a08… noreply 95 return None
0981a08… noreply 96
0981a08… noreply 97 @property
0981a08… noreply 98 def resolved_account_id(self) -> Optional[str]:
0981a08… noreply 99 return (
0981a08… noreply 100 self.account_id
0981a08… noreply 101 or (os.environ.get(self.account_id_env, "") if self.account_id_env else None)
0981a08… noreply 102 or None
0981a08… noreply 103 )
0981a08… noreply 104
0981a08… noreply 105 @property
0981a08… noreply 106 def resolved_token_path(self) -> Path:
0981a08… noreply 107 return self.token_path or TOKEN_DIR / f"{self.service}_token.json"
0981a08… noreply 108
0981a08… noreply 109 @property
0981a08… noreply 110 def supports_oauth(self) -> bool:
0981a08… noreply 111 return bool(self.oauth_authorize_url and self.oauth_token_url)
0981a08… noreply 112
0981a08… noreply 113
0981a08… noreply 114 @dataclass
0981a08… noreply 115 class AuthResult:
0981a08… noreply 116 """Result of an authentication attempt."""
0981a08… noreply 117
0981a08… noreply 118 success: bool
0981a08… noreply 119 access_token: Optional[str] = None
0981a08… noreply 120 method: Optional[str] = None # "saved_token", "oauth_pkce", "client_credentials", "api_key"
0981a08… noreply 121 expires_at: Optional[float] = None
0981a08… noreply 122 refresh_token: Optional[str] = None
0981a08… noreply 123 error: Optional[str] = None
0981a08… noreply 124
0981a08… noreply 125
0981a08… noreply 126 class OAuthManager:
0981a08… noreply 127 """Manages OAuth and API key authentication for a service.
0981a08… noreply 128
0981a08… noreply 129 Tries auth methods in order:
0981a08… noreply 130 1. Load saved token (refresh if expired)
0981a08… noreply 131 2. Client Credentials grant (if account_id is set)
0981a08… noreply 132 3. OAuth2 Authorization Code with PKCE (interactive)
0981a08… noreply 133 4. API key fallback
0981a08… noreply 134 """
0981a08… noreply 135
0981a08… noreply 136 def __init__(self, config: AuthConfig):
0981a08… noreply 137 self.config = config
0981a08… noreply 138 self._token_data: Optional[Dict] = None
0981a08… noreply 139
0981a08… noreply 140 def authenticate(self) -> AuthResult:
0981a08… noreply 141 """Run the auth chain and return the result."""
0981a08… noreply 142 # 1. Saved token
0981a08… noreply 143 result = self._try_saved_token()
0981a08… noreply 144 if result.success:
0981a08… noreply 145 return result
0981a08… noreply 146
0981a08… noreply 147 # 2. Client Credentials (Server-to-Server)
0981a08… noreply 148 if self.config.resolved_account_id and self.config.supports_oauth:
0981a08… noreply 149 result = self._try_client_credentials()
0981a08… noreply 150 if result.success:
0981a08… noreply 151 return result
0981a08… noreply 152
0981a08… noreply 153 # 3. OAuth PKCE (interactive)
0981a08… noreply 154 if self.config.supports_oauth and self.config.resolved_client_id:
0981a08… noreply 155 result = self._try_oauth_pkce()
0981a08… noreply 156 if result.success:
0981a08… noreply 157 return result
0981a08… noreply 158
0981a08… noreply 159 # 4. API key fallback
0981a08… noreply 160 api_key = self.config.resolved_api_key
0981a08… noreply 161 if api_key:
0981a08… noreply 162 return AuthResult(
0981a08… noreply 163 success=True,
0981a08… noreply 164 access_token=api_key,
0981a08… noreply 165 method="api_key",
0981a08… noreply 166 )
0981a08… noreply 167
0981a08… noreply 168 # Build a helpful error message
0981a08… noreply 169 hints = []
0981a08… noreply 170 if self.config.supports_oauth and self.config.client_id_env:
0981a08… noreply 171 hints.append(f"Set {self.config.client_id_env} for OAuth")
0981a08… noreply 172 if self.config.client_secret_env:
0981a08… noreply 173 hints.append(f"and {self.config.client_secret_env}")
0981a08… noreply 174 if self.config.api_key_env:
0981a08… noreply 175 hints.append(f"or set {self.config.api_key_env} for API key access")
0981a08… noreply 176 hint_str = (" (" + " ".join(hints) + ")") if hints else ""
0981a08… noreply 177
0981a08… noreply 178 return AuthResult(
0981a08… noreply 179 success=False,
0981a08… noreply 180 error=f"No auth method available for {self.config.service}.{hint_str}",
0981a08… noreply 181 )
0981a08… noreply 182
0981a08… noreply 183 def get_token(self) -> Optional[str]:
0981a08… noreply 184 """Convenience: authenticate and return just the token."""
0981a08… noreply 185 result = self.authenticate()
0981a08… noreply 186 return result.access_token if result.success else None
0981a08… noreply 187
0981a08… noreply 188 def _try_saved_token(self) -> AuthResult:
0981a08… noreply 189 """Load and validate a saved token."""
0981a08… noreply 190 token_path = self.config.resolved_token_path
0981a08… noreply 191 if not token_path.exists():
0981a08… noreply 192 return AuthResult(success=False)
0981a08… noreply 193
0981a08… noreply 194 try:
0981a08… noreply 195 data = json.loads(token_path.read_text())
0981a08… noreply 196 expires_at = data.get("expires_at", 0)
0981a08… noreply 197
0981a08… noreply 198 if time.time() < expires_at:
0981a08… noreply 199 self._token_data = data
0981a08… noreply 200 return AuthResult(
0981a08… noreply 201 success=True,
0981a08… noreply 202 access_token=data["access_token"],
0981a08… noreply 203 method="saved_token",
0981a08… noreply 204 expires_at=expires_at,
0981a08… noreply 205 )
0981a08… noreply 206
0981a08… noreply 207 # Expired — try refresh
0981a08… noreply 208 if data.get("refresh_token"):
0981a08… noreply 209 return self._refresh_token(data)
0981a08… noreply 210
0981a08… noreply 211 return AuthResult(success=False)
0981a08… noreply 212 except Exception as exc:
0981a08… noreply 213 logger.debug("Failed to load saved token for %s: %s", self.config.service, exc)
0981a08… noreply 214 return AuthResult(success=False)
0981a08… noreply 215
0981a08… noreply 216 def _refresh_token(self, data: Dict) -> AuthResult:
0981a08… noreply 217 """Refresh an expired OAuth token."""
0981a08… noreply 218 try:
0981a08… noreply 219 import requests
0981a08… noreply 220 except ImportError:
0981a08… noreply 221 return AuthResult(success=False, error="requests not installed")
0981a08… noreply 222
0981a08… noreply 223 client_id = data.get("client_id") or self.config.resolved_client_id
0981a08… noreply 224 client_secret = data.get("client_secret") or self.config.resolved_client_secret
0981a08… noreply 225
0981a08… noreply 226 if not client_id or not data.get("refresh_token"):
0981a08… noreply 227 return AuthResult(success=False)
0981a08… noreply 228
0981a08… noreply 229 try:
0981a08… noreply 230 resp = requests.post(
0981a08… noreply 231 self.config.oauth_token_url,
0981a08… noreply 232 data={
0981a08… noreply 233 "grant_type": "refresh_token",
0981a08… noreply 234 "refresh_token": data["refresh_token"],
0981a08… noreply 235 },
0981a08… noreply 236 auth=(client_id, client_secret or ""),
0981a08… noreply 237 timeout=30,
0981a08… noreply 238 )
0981a08… noreply 239 resp.raise_for_status()
0981a08… noreply 240 token_data = resp.json()
0981a08… noreply 241
0981a08… noreply 242 new_data = {
0981a08… noreply 243 "access_token": token_data["access_token"],
0981a08… noreply 244 "refresh_token": token_data.get("refresh_token", data["refresh_token"]),
0981a08… noreply 245 "expires_at": time.time() + token_data.get("expires_in", 3600) - 60,
0981a08… noreply 246 "client_id": client_id,
0981a08… noreply 247 "client_secret": client_secret or "",
0981a08… noreply 248 }
0981a08… noreply 249 self._save_token(new_data)
0981a08… noreply 250 self._token_data = new_data
0981a08… noreply 251
0981a08… noreply 252 logger.info("Refreshed OAuth token for %s", self.config.service)
0981a08… noreply 253 return AuthResult(
0981a08… noreply 254 success=True,
0981a08… noreply 255 access_token=new_data["access_token"],
0981a08… noreply 256 method="saved_token",
0981a08… noreply 257 expires_at=new_data["expires_at"],
0981a08… noreply 258 refresh_token=new_data["refresh_token"],
0981a08… noreply 259 )
0981a08… noreply 260 except Exception as exc:
0981a08… noreply 261 logger.debug("Token refresh failed for %s: %s", self.config.service, exc)
0981a08… noreply 262 return AuthResult(success=False)
0981a08… noreply 263
0981a08… noreply 264 def _try_client_credentials(self) -> AuthResult:
0981a08… noreply 265 """Server-to-Server OAuth using client credentials grant."""
0981a08… noreply 266 try:
0981a08… noreply 267 import requests
0981a08… noreply 268 except ImportError:
0981a08… noreply 269 return AuthResult(success=False, error="requests not installed")
0981a08… noreply 270
0981a08… noreply 271 client_id = self.config.resolved_client_id
0981a08… noreply 272 client_secret = self.config.resolved_client_secret
0981a08… noreply 273 account_id = self.config.resolved_account_id
0981a08… noreply 274
0981a08… noreply 275 if not client_id or not client_secret:
0981a08… noreply 276 return AuthResult(success=False)
0981a08… noreply 277
0981a08… noreply 278 try:
0981a08… noreply 279 resp = requests.post(
0981a08… noreply 280 self.config.oauth_token_url,
0981a08… noreply 281 params={
0981a08… noreply 282 "grant_type": "account_credentials",
0981a08… noreply 283 "account_id": account_id,
0981a08… noreply 284 },
0981a08… noreply 285 auth=(client_id, client_secret),
0981a08… noreply 286 timeout=30,
0981a08… noreply 287 )
0981a08… noreply 288 resp.raise_for_status()
0981a08… noreply 289 token_data = resp.json()
0981a08… noreply 290
0981a08… noreply 291 data = {
0981a08… noreply 292 "access_token": token_data["access_token"],
0981a08… noreply 293 "expires_at": time.time() + token_data.get("expires_in", 3600) - 60,
0981a08… noreply 294 }
0981a08… noreply 295 self._save_token(data)
0981a08… noreply 296 self._token_data = data
0981a08… noreply 297
0981a08… noreply 298 logger.info("Authenticated %s via client credentials", self.config.service)
0981a08… noreply 299 return AuthResult(
0981a08… noreply 300 success=True,
0981a08… noreply 301 access_token=data["access_token"],
0981a08… noreply 302 method="client_credentials",
0981a08… noreply 303 expires_at=data["expires_at"],
0981a08… noreply 304 )
0981a08… noreply 305 except Exception as exc:
0981a08… noreply 306 logger.debug("Client credentials failed for %s: %s", self.config.service, exc)
0981a08… noreply 307 return AuthResult(success=False)
0981a08… noreply 308
0981a08… noreply 309 def _try_oauth_pkce(self) -> AuthResult:
0981a08… noreply 310 """Interactive OAuth2 Authorization Code flow with PKCE."""
0981a08… noreply 311 try:
0981a08… noreply 312 import requests
0981a08… noreply 313 except ImportError:
0981a08… noreply 314 return AuthResult(success=False, error="requests not installed")
0981a08… noreply 315
0981a08… noreply 316 client_id = self.config.resolved_client_id
0981a08… noreply 317 if not client_id:
0981a08… noreply 318 return AuthResult(success=False)
0981a08… noreply 319
0981a08… noreply 320 # Generate PKCE verifier and challenge
0981a08… noreply 321 code_verifier = secrets.token_urlsafe(64)
0981a08… noreply 322 code_challenge = (
0981a08… noreply 323 base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode("ascii")).digest())
0981a08… noreply 324 .rstrip(b"=")
0981a08… noreply 325 .decode("ascii")
0981a08… noreply 326 )
0981a08… noreply 327
0981a08… noreply 328 # Build authorize URL
0981a08… noreply 329 params = (
0981a08… noreply 330 f"?response_type=code"
0981a08… noreply 331 f"&client_id={client_id}"
0981a08… noreply 332 f"&redirect_uri={self.config.redirect_uri}"
0981a08… noreply 333 f"&code_challenge={code_challenge}"
0981a08… noreply 334 f"&code_challenge_method=S256"
0981a08… noreply 335 )
0981a08… noreply 336 if self.config.scopes:
0981a08… noreply 337 params += f"&scope={'+'.join(self.config.scopes)}"
0981a08… noreply 338
0981a08… noreply 339 authorize_url = f"{self.config.oauth_authorize_url}{params}"
0981a08… noreply 340
0981a08… noreply 341 print(f"\nOpen this URL to authorize PlanOpticon ({self.config.service}):")
0981a08… noreply 342 print(f"{authorize_url}\n")
0981a08… noreply 343
0981a08… noreply 344 try:
0981a08… noreply 345 webbrowser.open(authorize_url)
0981a08… noreply 346 except Exception:
0981a08… noreply 347 pass
0981a08… noreply 348
0981a08… noreply 349 try:
0981a08… noreply 350 auth_code = input("Enter the authorization code: ").strip()
0981a08… noreply 351 except (KeyboardInterrupt, EOFError):
0981a08… noreply 352 return AuthResult(success=False, error="Auth cancelled by user")
0981a08… noreply 353
0981a08… noreply 354 if not auth_code:
0981a08… noreply 355 return AuthResult(success=False, error="No auth code provided")
0981a08… noreply 356
0981a08… noreply 357 # Exchange code for tokens
0981a08… noreply 358 client_secret = self.config.resolved_client_secret
0981a08… noreply 359 try:
0981a08… noreply 360 resp = requests.post(
0981a08… noreply 361 self.config.oauth_token_url,
0981a08… noreply 362 data={
0981a08… noreply 363 "grant_type": "authorization_code",
0981a08… noreply 364 "code": auth_code,
0981a08… noreply 365 "redirect_uri": self.config.redirect_uri,
0981a08… noreply 366 "code_verifier": code_verifier,
0981a08… noreply 367 },
0981a08… noreply 368 auth=(client_id, client_secret or ""),
0981a08… noreply 369 timeout=30,
0981a08… noreply 370 )
0981a08… noreply 371 resp.raise_for_status()
0981a08… noreply 372 token_data = resp.json()
0981a08… noreply 373
0981a08… noreply 374 data = {
0981a08… noreply 375 "access_token": token_data["access_token"],
0981a08… noreply 376 "refresh_token": token_data.get("refresh_token"),
0981a08… noreply 377 "expires_at": time.time() + token_data.get("expires_in", 3600) - 60,
0981a08… noreply 378 "client_id": client_id,
0981a08… noreply 379 "client_secret": client_secret or "",
0981a08… noreply 380 }
0981a08… noreply 381 self._save_token(data)
0981a08… noreply 382 self._token_data = data
0981a08… noreply 383
0981a08… noreply 384 logger.info("Authenticated %s via OAuth PKCE", self.config.service)
0981a08… noreply 385 return AuthResult(
0981a08… noreply 386 success=True,
0981a08… noreply 387 access_token=data["access_token"],
0981a08… noreply 388 method="oauth_pkce",
0981a08… noreply 389 expires_at=data["expires_at"],
0981a08… noreply 390 refresh_token=data.get("refresh_token"),
0981a08… noreply 391 )
0981a08… noreply 392 except Exception as exc:
0981a08… noreply 393 logger.debug("OAuth PKCE failed for %s: %s", self.config.service, exc)
0981a08… noreply 394 return AuthResult(success=False, error=str(exc))
0981a08… noreply 395
0981a08… noreply 396 def _save_token(self, data: Dict) -> None:
0981a08… noreply 397 """Persist token data to disk."""
0981a08… noreply 398 token_path = self.config.resolved_token_path
0981a08… noreply 399 token_path.parent.mkdir(parents=True, exist_ok=True)
0981a08… noreply 400 token_path.write_text(json.dumps(data))
0981a08… noreply 401 logger.info("Saved %s token to %s", self.config.service, token_path)
0981a08… noreply 402
0981a08… noreply 403 def clear_token(self) -> None:
0981a08… noreply 404 """Remove saved token (logout)."""
0981a08… noreply 405 token_path = self.config.resolved_token_path
0981a08… noreply 406 if token_path.exists():
0981a08… noreply 407 token_path.unlink()
0981a08… noreply 408 logger.info("Cleared %s token", self.config.service)
0981a08… noreply 409
0981a08… noreply 410
0981a08… noreply 411 # -----------------------------------------------------------------------
0981a08… noreply 412 # Pre-built configs for known services
0981a08… noreply 413 # -----------------------------------------------------------------------
0981a08… noreply 414
0981a08… noreply 415 KNOWN_CONFIGS: Dict[str, AuthConfig] = {
0981a08… noreply 416 "zoom": AuthConfig(
0981a08… noreply 417 service="zoom",
0981a08… noreply 418 oauth_authorize_url="https://zoom.us/oauth/authorize",
0981a08… noreply 419 oauth_token_url="https://zoom.us/oauth/token",
0981a08… noreply 420 client_id_env="ZOOM_CLIENT_ID",
0981a08… noreply 421 client_secret_env="ZOOM_CLIENT_SECRET",
0981a08… noreply 422 account_id_env="ZOOM_ACCOUNT_ID",
0981a08… noreply 423 ),
0981a08… noreply 424 "notion": AuthConfig(
0981a08… noreply 425 service="notion",
0981a08… noreply 426 oauth_authorize_url="https://api.notion.com/v1/oauth/authorize",
0981a08… noreply 427 oauth_token_url="https://api.notion.com/v1/oauth/token",
0981a08… noreply 428 client_id_env="NOTION_CLIENT_ID",
0981a08… noreply 429 client_secret_env="NOTION_CLIENT_SECRET",
0981a08… noreply 430 api_key_env="NOTION_API_KEY",
0981a08… noreply 431 ),
0981a08… noreply 432 "dropbox": AuthConfig(
0981a08… noreply 433 service="dropbox",
0981a08… noreply 434 oauth_authorize_url="https://www.dropbox.com/oauth2/authorize",
0981a08… noreply 435 oauth_token_url="https://api.dropboxapi.com/oauth2/token",
0981a08… noreply 436 client_id_env="DROPBOX_APP_KEY",
0981a08… noreply 437 client_secret_env="DROPBOX_APP_SECRET",
0981a08… noreply 438 api_key_env="DROPBOX_ACCESS_TOKEN",
0981a08… noreply 439 ),
0981a08… noreply 440 "github": AuthConfig(
0981a08… noreply 441 service="github",
0981a08… noreply 442 oauth_authorize_url="https://github.com/login/oauth/authorize",
0981a08… noreply 443 oauth_token_url="https://github.com/login/oauth/access_token",
0981a08… noreply 444 client_id_env="GITHUB_CLIENT_ID",
0981a08… noreply 445 client_secret_env="GITHUB_CLIENT_SECRET",
0981a08… noreply 446 api_key_env="GITHUB_TOKEN",
0981a08… noreply 447 scopes=["repo", "read:org"],
0981a08… noreply 448 ),
0981a08… noreply 449 "google": AuthConfig(
0981a08… noreply 450 service="google",
0981a08… noreply 451 oauth_authorize_url="https://accounts.google.com/o/oauth2/v2/auth",
0981a08… noreply 452 oauth_token_url="https://oauth2.googleapis.com/token",
0981a08… noreply 453 client_id_env="GOOGLE_CLIENT_ID",
0981a08… noreply 454 client_secret_env="GOOGLE_CLIENT_SECRET",
0981a08… noreply 455 api_key_env="GOOGLE_API_KEY",
0981a08… noreply 456 scopes=[
0981a08… noreply 457 "https://www.googleapis.com/auth/drive.readonly",
0981a08… noreply 458 "https://www.googleapis.com/auth/documents.readonly",
0981a08… noreply 459 ],
0981a08… noreply 460 ),
0981a08… noreply 461 "microsoft": AuthConfig(
0981a08… noreply 462 service="microsoft",
0981a08… noreply 463 oauth_authorize_url=("https://login.microsoftonline.com/common/oauth2/v2.0/authorize"),
0981a08… noreply 464 oauth_token_url=("https://login.microsoftonline.com/common/oauth2/v2.0/token"),
0981a08… noreply 465 client_id_env="MICROSOFT_CLIENT_ID",
0981a08… noreply 466 client_secret_env="MICROSOFT_CLIENT_SECRET",
0981a08… noreply 467 scopes=[
0981a08… noreply 468 "https://graph.microsoft.com/OnlineMeetings.Read",
0981a08… noreply 469 "https://graph.microsoft.com/Files.Read",
0981a08… noreply 470 ],
0981a08… noreply 471 ),
0981a08… noreply 472 }
0981a08… noreply 473
0981a08… noreply 474
0981a08… noreply 475 def get_auth_config(service: str) -> Optional[AuthConfig]:
0981a08… noreply 476 """Get a pre-built AuthConfig for a known service."""
0981a08… noreply 477 return KNOWN_CONFIGS.get(service)
0981a08… noreply 478
0981a08… noreply 479
0981a08… noreply 480 def get_auth_manager(service: str) -> Optional[OAuthManager]:
0981a08… noreply 481 """Get an OAuthManager for a known service."""
0981a08… noreply 482 config = get_auth_config(service)
0981a08… noreply 483 if config:
0981a08… noreply 484 return OAuthManager(config)
0981a08… noreply 485 return None

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button