orchestr8-platform 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
orchestr8/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "3.0.0"
@@ -0,0 +1,16 @@
1
+ """Authentication module for Orchestr8."""
2
+
3
+ from .github_oauth import GitHubDeviceFlow, GitHubAuthResult
4
+ from .keycloak_idp import (
5
+ KeycloakIdentityProviderManager,
6
+ IdentityProviderType,
7
+ IdentityProviderConfig,
8
+ )
9
+
10
+ __all__ = [
11
+ "GitHubDeviceFlow",
12
+ "GitHubAuthResult",
13
+ "KeycloakIdentityProviderManager",
14
+ "IdentityProviderType",
15
+ "IdentityProviderConfig",
16
+ ]
@@ -0,0 +1,215 @@
1
+ """GitHub OAuth Device Flow implementation for Orchestr8."""
2
+
3
+ import time
4
+ import webbrowser
5
+ from typing import Dict, Optional
6
+ from dataclasses import dataclass
7
+ import requests
8
+ from rich.console import Console
9
+ from rich.panel import Panel
10
+ from rich.progress import Progress, SpinnerColumn, TextColumn
11
+
12
+
13
+ @dataclass
14
+ class GitHubAuthResult:
15
+ """Result of GitHub authentication."""
16
+
17
+ access_token: str
18
+ token_type: str
19
+ scope: str
20
+
21
+
22
+ class GitHubDeviceFlow:
23
+ """GitHub OAuth Device Flow implementation."""
24
+
25
+ # GitHub OAuth endpoints
26
+ DEVICE_CODE_URL = "https://github.com/login/device/code"
27
+ TOKEN_URL = "https://github.com/login/oauth/access_token"
28
+
29
+ # Default Orchestr8 OAuth App Client ID
30
+ DEFAULT_CLIENT_ID = "Ov23liefXYUwEpx4AMWz"
31
+
32
+ def __init__(
33
+ self, console: Optional[Console] = None, client_id: Optional[str] = None
34
+ ):
35
+ self.console = console or Console()
36
+ self.client_id = client_id or self.DEFAULT_CLIENT_ID
37
+
38
+ def authenticate(self, scopes: Optional[str] = None) -> Optional[GitHubAuthResult]:
39
+ """
40
+ Perform GitHub OAuth device flow authentication.
41
+
42
+ Args:
43
+ scopes: OAuth scopes to request (e.g., "repo", "user:email")
44
+
45
+ Returns:
46
+ GitHubAuthResult with access token if successful, None otherwise
47
+ """
48
+ # Request device code
49
+ device_code_data = self._request_device_code(scopes)
50
+ if not device_code_data:
51
+ return None
52
+
53
+ # Show user instructions
54
+ self._display_auth_instructions(
55
+ device_code_data["user_code"], device_code_data["verification_uri"]
56
+ )
57
+
58
+ # Poll for token
59
+ token_data = self._poll_for_token(
60
+ device_code_data["device_code"], device_code_data["interval"]
61
+ )
62
+
63
+ if token_data:
64
+ return GitHubAuthResult(
65
+ access_token=token_data["access_token"],
66
+ token_type=token_data.get("token_type", "bearer"),
67
+ scope=token_data.get("scope", ""),
68
+ )
69
+
70
+ return None
71
+
72
+ def _request_device_code(self, scopes: Optional[str]) -> Optional[Dict]:
73
+ """Request device and user codes from GitHub."""
74
+ headers = {"Accept": "application/json"}
75
+ data = {"client_id": self.client_id}
76
+
77
+ if scopes:
78
+ data["scope"] = scopes
79
+
80
+ try:
81
+ response = requests.post(self.DEVICE_CODE_URL, headers=headers, data=data)
82
+ response.raise_for_status()
83
+ return response.json()
84
+ except requests.exceptions.RequestException as e:
85
+ self.console.print(f"[red]Failed to request device code: {e}[/red]")
86
+ return None
87
+
88
+ def _display_auth_instructions(self, user_code: str, verification_uri: str):
89
+ """Display authentication instructions to the user."""
90
+ self.console.print()
91
+ panel = Panel(
92
+ f"[bold cyan]Please visit:[/bold cyan] {verification_uri}\n"
93
+ f"[bold cyan]Enter code:[/bold cyan] [bold yellow]{user_code}[/bold yellow]",
94
+ title="🔐 GitHub Authentication Required",
95
+ border_style="cyan",
96
+ )
97
+ self.console.print(panel)
98
+
99
+ # Try to open browser automatically
100
+ try:
101
+ webbrowser.open(verification_uri)
102
+ except Exception:
103
+ pass
104
+
105
+ def _poll_for_token(self, device_code: str, interval: int) -> Optional[Dict]:
106
+ """Poll GitHub for access token."""
107
+ headers = {"Accept": "application/json"}
108
+ data = {
109
+ "client_id": self.client_id,
110
+ "device_code": device_code,
111
+ "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
112
+ }
113
+
114
+ with Progress(
115
+ SpinnerColumn(),
116
+ TextColumn("[progress.description]{task.description}"),
117
+ console=self.console,
118
+ transient=True,
119
+ ) as progress:
120
+ task = progress.add_task("Waiting for authentication...", total=None)
121
+
122
+ while True:
123
+ try:
124
+ response = requests.post(self.TOKEN_URL, headers=headers, data=data)
125
+ response_data = response.json()
126
+
127
+ if "access_token" in response_data:
128
+ progress.update(task, completed=True)
129
+ return response_data
130
+
131
+ error = response_data.get("error")
132
+
133
+ if error == "authorization_pending":
134
+ # Still waiting for user to authorize
135
+ time.sleep(interval)
136
+
137
+ elif error == "slow_down":
138
+ # Rate limited, increase interval
139
+ interval = response_data.get("interval", interval + 5)
140
+ time.sleep(interval)
141
+
142
+ elif error == "expired_token":
143
+ progress.update(
144
+ task, description="[red]❌ Authentication expired[/red]"
145
+ )
146
+ self.console.print(
147
+ "[red]The device code has expired. Please try again.[/red]"
148
+ )
149
+ return None
150
+
151
+ elif error == "access_denied":
152
+ progress.update(task, description="[red]❌ Access denied[/red]")
153
+ self.console.print("[red]Authentication was denied.[/red]")
154
+ return None
155
+
156
+ else:
157
+ # Unknown error
158
+ progress.update(
159
+ task, description=f"[red]❌ Error: {error}[/red]"
160
+ )
161
+ return None
162
+
163
+ except requests.exceptions.RequestException as e:
164
+ progress.update(task, description="[red]❌ Network error[/red]")
165
+ self.console.print(f"[red]Network error: {e}[/red]")
166
+ return None
167
+
168
+ @staticmethod
169
+ def recommend_scopes(private_repos: bool = True) -> str:
170
+ """
171
+ Recommend OAuth scopes based on usage.
172
+
173
+ Args:
174
+ private_repos: Whether access to private repositories is needed
175
+
176
+ Returns:
177
+ Recommended scope string
178
+ """
179
+ if private_repos:
180
+ return "repo" # Full repo access including private repos
181
+ else:
182
+ return "" # No scope needed for public repos
183
+
184
+ def validate_token(self, token: str) -> bool:
185
+ """
186
+ Validate that a token has access to the required repository.
187
+
188
+ Args:
189
+ token: GitHub access token to validate
190
+
191
+ Returns:
192
+ True if token is valid and has repo access
193
+ """
194
+ headers = {
195
+ "Authorization": f"token {token}",
196
+ "Accept": "application/vnd.github.v3+json",
197
+ }
198
+
199
+ try:
200
+ # Check token validity and scopes
201
+ response = requests.get("https://api.github.com/user", headers=headers)
202
+
203
+ if response.status_code == 200:
204
+ # Check scopes from response headers (for debugging)
205
+ # scopes = response.headers.get("X-OAuth-Scopes", "")
206
+ return True
207
+ else:
208
+ self.console.print(
209
+ f"[red]Token validation failed: {response.status_code}[/red]"
210
+ )
211
+ return False
212
+
213
+ except requests.exceptions.RequestException as e:
214
+ self.console.print(f"[red]Error validating token: {e}[/red]")
215
+ return False