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 +1 -0
- orchestr8/auth/__init__.py +16 -0
- orchestr8/auth/github_oauth.py +215 -0
- orchestr8/auth/keycloak_idp.py +519 -0
- orchestr8/cli/module.py +469 -0
- orchestr8/cli.py +1174 -0
- orchestr8/commands/argocd.py +484 -0
- orchestr8/commands/bootstrap.py +1582 -0
- orchestr8/commands/branch.py +387 -0
- orchestr8/commands/doctor.py +374 -0
- orchestr8/commands/module.py +763 -0
- orchestr8/commands/secrets.py +996 -0
- orchestr8/core/__init__.py +7 -0
- orchestr8/core/config.py +85 -0
- orchestr8/core/orchestrator.py +1794 -0
- orchestr8/core/secrets.py +150 -0
- orchestr8/core/terraform.py +551 -0
- orchestr8/core/validator.py +567 -0
- orchestr8/providers/__init__.py +7 -0
- orchestr8/providers/aws.py +89 -0
- orchestr8/providers/azure.py +567 -0
- orchestr8/providers/github.py +97 -0
- orchestr8/providers/local.py +73 -0
- orchestr8/sdk.py +156 -0
- orchestr8/utils/azure_cli.py +117 -0
- orchestr8_platform-3.0.0.dist-info/METADATA +164 -0
- orchestr8_platform-3.0.0.dist-info/RECORD +30 -0
- orchestr8_platform-3.0.0.dist-info/WHEEL +5 -0
- orchestr8_platform-3.0.0.dist-info/entry_points.txt +2 -0
- orchestr8_platform-3.0.0.dist-info/top_level.txt +1 -0
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
|