Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -14,10 +14,7 @@ script_dir = Path(__file__).resolve().parent
|
|
| 14 |
env_path = script_dir / '.env'
|
| 15 |
load_dotenv(dotenv_path=env_path)
|
| 16 |
|
| 17 |
-
|
| 18 |
-
env_path = script_dir / '.env'
|
| 19 |
-
load_dotenv(dotenv_path=env_path)
|
| 20 |
-
|
| 21 |
def fetch_api_endpoints_yaml(spec_url):
|
| 22 |
try:
|
| 23 |
response = requests.get(spec_url)
|
|
@@ -39,21 +36,17 @@ def fetch_api_endpoints_yaml(spec_url):
|
|
| 39 |
if not methods or not isinstance(methods, dict):
|
| 40 |
continue
|
| 41 |
|
| 42 |
-
# Get common parameters defined at path level
|
| 43 |
common_params = methods.get("parameters", [])
|
| 44 |
-
|
| 45 |
for method, details in methods.items():
|
| 46 |
if method.lower() not in valid_methods:
|
| 47 |
continue
|
| 48 |
|
| 49 |
-
# Combine path-level and method-level parameters
|
| 50 |
method_params = details.get("parameters", [])
|
| 51 |
all_params = common_params + method_params
|
| 52 |
-
|
| 53 |
endpoint_info = {
|
| 54 |
"summary": details.get("summary", ""),
|
| 55 |
"description": details.get("description", ""),
|
| 56 |
-
"parameters": all_params
|
| 57 |
}
|
| 58 |
endpoints[path][method.lower()] = endpoint_info
|
| 59 |
return endpoints
|
|
@@ -78,21 +71,17 @@ def fetch_api_endpoints_json(spec_url):
|
|
| 78 |
if not methods or not isinstance(methods, dict):
|
| 79 |
continue
|
| 80 |
|
| 81 |
-
# Get common parameters defined at path level
|
| 82 |
common_params = methods.get("parameters", [])
|
| 83 |
-
|
| 84 |
for method, details in methods.items():
|
| 85 |
if method.lower() not in valid_methods:
|
| 86 |
continue
|
| 87 |
|
| 88 |
-
# Combine path-level and method-level parameters
|
| 89 |
method_params = details.get("parameters", [])
|
| 90 |
all_params = common_params + method_params
|
| 91 |
-
|
| 92 |
endpoint_info = {
|
| 93 |
"summary": details.get("summary", ""),
|
| 94 |
"description": details.get("description", ""),
|
| 95 |
-
"parameters": all_params
|
| 96 |
}
|
| 97 |
endpoints[path][method.lower()] = endpoint_info
|
| 98 |
return endpoints
|
|
@@ -111,790 +100,740 @@ def get_endpoints(spec_choice):
|
|
| 111 |
return fetch_api_endpoints_yaml(spec_url)
|
| 112 |
|
| 113 |
def group_endpoints(endpoints, spec_choice):
|
| 114 |
-
"""Group endpoints with special handling for Okta API"""
|
| 115 |
groups = {}
|
| 116 |
-
|
| 117 |
-
# Special handling for Okta endpoints
|
| 118 |
if spec_choice == "Okta (JSON)":
|
| 119 |
for path, methods in endpoints.items():
|
| 120 |
-
# Remove /api/v1/ prefix and get the first segment
|
| 121 |
clean_path = path.replace('/api/v1/', '')
|
| 122 |
segments = clean_path.strip("/").split("/")
|
| 123 |
group_key = segments[0] if segments else "other"
|
| 124 |
-
|
| 125 |
if group_key not in groups:
|
| 126 |
groups[group_key] = {}
|
| 127 |
groups[group_key][path] = methods
|
| 128 |
else:
|
| 129 |
-
# Original grouping logic for other APIs
|
| 130 |
for path, methods in endpoints.items():
|
| 131 |
segments = path.strip("/").split("/")
|
| 132 |
group_key = segments[0] if segments[0] != "" else "other"
|
| 133 |
if group_key not in groups:
|
| 134 |
groups[group_key] = {}
|
| 135 |
groups[group_key][path] = methods
|
| 136 |
-
|
| 137 |
return groups
|
| 138 |
|
| 139 |
def verify_credentials(spec_choice, api_base_url, grant_type=None, client_id=None,
|
| 140 |
client_secret=None, api_token=None, iiq_username=None, iiq_password=None):
|
| 141 |
-
"""Verify API credentials using existing handlers"""
|
| 142 |
-
|
| 143 |
-
# Base URL validation
|
| 144 |
if not api_base_url or not api_base_url.strip():
|
| 145 |
return (
|
| 146 |
-
gr.update(value="
|
| 147 |
-
False,
|
| 148 |
-
False
|
| 149 |
)
|
| 150 |
|
| 151 |
try:
|
| 152 |
-
# Credential validation based on API type
|
| 153 |
if spec_choice == "Okta (JSON)":
|
| 154 |
if not api_token or not api_token.strip():
|
| 155 |
return (
|
| 156 |
-
gr.update(value="
|
| 157 |
False,
|
| 158 |
False
|
| 159 |
)
|
| 160 |
-
|
| 161 |
-
# Test Okta connection
|
| 162 |
result = handle_okta_call(api_base_url, api_token, "", {}, ["/api/v1/users/me"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
|
| 164 |
elif spec_choice == "SailPoint IdentityNow (YAML)":
|
| 165 |
-
|
| 166 |
-
if not all([
|
| 167 |
-
grant_type and grant_type.strip(),
|
| 168 |
-
client_id and client_id.strip(),
|
| 169 |
-
client_secret and client_secret.strip()
|
| 170 |
-
]):
|
| 171 |
return (
|
| 172 |
-
gr.update(value="
|
| 173 |
False,
|
| 174 |
False
|
| 175 |
)
|
|
|
|
| 176 |
|
| 177 |
-
#
|
| 178 |
-
result
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
|
| 188 |
elif spec_choice == "Sailpoint IIQ (YAML)":
|
| 189 |
-
|
| 190 |
-
if not all([
|
| 191 |
-
iiq_username and iiq_username.strip(),
|
| 192 |
-
iiq_password and iiq_password.strip()
|
| 193 |
-
]):
|
| 194 |
return (
|
| 195 |
-
gr.update(value="
|
| 196 |
False,
|
| 197 |
False
|
| 198 |
)
|
|
|
|
| 199 |
|
| 200 |
-
#
|
| 201 |
-
result
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
|
|
|
| 230 |
|
| 231 |
-
#
|
| 232 |
return (
|
| 233 |
-
gr.update(value="
|
| 234 |
-
True,
|
| 235 |
-
True
|
| 236 |
)
|
| 237 |
|
| 238 |
except requests.exceptions.ConnectionError:
|
| 239 |
return (
|
| 240 |
-
gr.update(value="
|
| 241 |
False,
|
| 242 |
False
|
| 243 |
)
|
| 244 |
except Exception as e:
|
| 245 |
return (
|
| 246 |
-
gr.update(value=f"
|
| 247 |
False,
|
| 248 |
False
|
| 249 |
)
|
| 250 |
|
| 251 |
-
# CSS
|
| 252 |
custom_css = """
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
.
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
"""
|
| 347 |
-
text_box_css = """
|
| 348 |
-
#api_base_url {
|
| 349 |
-
background: gr.themes.colors.red;
|
| 350 |
-
}
|
| 351 |
-
"""
|
| 352 |
|
| 353 |
with gr.Blocks(css=custom_css) as demo:
|
| 354 |
-
gr.
|
| 355 |
|
| 356 |
-
# Session
|
| 357 |
session_id_state = gr.State("")
|
| 358 |
-
confirmed_endpoints_state = gr.State([])
|
| 359 |
-
display_values_state = gr.State([])
|
| 360 |
locked_endpoints_state = gr.State([])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 361 |
|
| 362 |
-
#
|
| 363 |
-
with gr.
|
| 364 |
-
gr.
|
| 365 |
-
|
| 366 |
-
choices=["Okta (JSON)", "SailPoint IdentityNow (YAML)", "Sailpoint IIQ (YAML)"],
|
| 367 |
-
value="SailPoint IdentityNow (YAML)",
|
| 368 |
-
label="Select an API",
|
| 369 |
-
filterable=True,
|
| 370 |
-
elem_id="api_choice"
|
| 371 |
-
)
|
| 372 |
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
gr.
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
with gr.Column(visible=False) as okta_auth:
|
| 384 |
-
api_token = gr.Textbox(label="API Token", placeholder="Enter your API Token", type="password", elem_id="api_base_url")
|
| 385 |
-
with gr.Column(visible=False) as iiq_auth:
|
| 386 |
-
iiq_username = gr.Textbox(label="Username", placeholder="Enter your IIQ Username", elem_id="api_base_url")
|
| 387 |
-
iiq_password = gr.Textbox(label="Password", placeholder="Enter your IIQ Password", type="password", elem_id="api_base_url")
|
| 388 |
-
|
| 389 |
-
verify_btn = gr.Button("Verify Credentials")
|
| 390 |
-
loading_indicator = gr.Markdown("", visible=False) # Add this
|
| 391 |
-
credentials_message = gr.Markdown("")
|
| 392 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 393 |
max_groups = 100
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
gr.Markdown("### Choose Endpoints")
|
| 397 |
accordion_placeholders = []
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
#
|
| 420 |
-
with gr.Group(elem_classes="section", visible=False) as
|
| 421 |
-
fetch_btn = gr.Button("Fetch Data", interactive=False)
|
| 422 |
-
fetch_loading_indicator = gr.Markdown("", visible=False) # Add loading indicator for fetch operation
|
| 423 |
-
# Results Section (hidden until data fetched)
|
| 424 |
-
with gr.Group(elem_classes="section", visible=False) as results_section:
|
| 425 |
gr.Markdown("### Results")
|
| 426 |
-
results_out = gr.JSON(label="API Responses")
|
| 427 |
-
download_out = gr.File(label="Download Session Data (ZIP)")
|
| 428 |
-
|
| 429 |
-
# Event
|
| 430 |
-
def
|
| 431 |
-
"""Update authentication fields and reset UI when API selection changes"""
|
| 432 |
-
# Update auth field visibility
|
| 433 |
-
auth_updates = {
|
| 434 |
-
"Okta (JSON)": [False, True, False],
|
| 435 |
-
"SailPoint IdentityNow (YAML)": [True, False, False],
|
| 436 |
-
"Sailpoint IIQ (YAML)": [False, False, True]
|
| 437 |
-
}
|
| 438 |
-
visibilities = auth_updates.get(api_choice, [False, False, False])
|
| 439 |
-
|
| 440 |
return [
|
| 441 |
-
|
| 442 |
-
gr.update(visible=
|
| 443 |
-
gr.update(visible=
|
| 444 |
-
gr.update(visible=False),
|
| 445 |
-
gr.update(
|
| 446 |
-
gr.update(
|
| 447 |
-
gr.update(value=
|
|
|
|
|
|
|
| 448 |
]
|
| 449 |
|
| 450 |
-
def verify_and_load_endpoints(
|
| 451 |
-
|
| 452 |
-
"""Verify credentials and load endpoints into accordions"""
|
| 453 |
-
# Show loading message first
|
| 454 |
yield (
|
| 455 |
-
gr.update(
|
| 456 |
-
gr.update(value=""
|
| 457 |
-
gr.update(visible=False),
|
| 458 |
-
gr.update(visible=False),
|
| 459 |
-
*[gr.update(visible=False, label="", value=[]) for _ in range(max_groups * 2)]
|
| 460 |
)
|
| 461 |
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
|
|
|
|
|
|
|
|
|
| 468 |
if not endpoints_vis:
|
| 469 |
yield (
|
| 470 |
-
gr.update(visible=False),
|
| 471 |
-
status,
|
| 472 |
-
gr.update(visible=False),
|
| 473 |
-
gr.update(visible=False),
|
| 474 |
*[gr.update(visible=False, label="", value=[]) for _ in range(max_groups * 2)]
|
| 475 |
)
|
| 476 |
return
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
if not endpoints:
|
| 481 |
-
yield (
|
| 482 |
-
gr.update(visible=False),
|
| 483 |
-
gr.update(value="No endpoints found", visible=True),
|
| 484 |
-
gr.update(visible=False),
|
| 485 |
-
gr.update(visible=False),
|
| 486 |
-
*[gr.update(visible=False, label="", value=[]) for _ in range(max_groups * 2)]
|
| 487 |
-
)
|
| 488 |
-
return
|
| 489 |
-
|
| 490 |
-
groups = group_endpoints(endpoints, spec_choice)
|
| 491 |
-
group_keys = list(groups.keys())
|
| 492 |
-
updates = []
|
| 493 |
-
|
| 494 |
-
# Process each group
|
| 495 |
-
for i in range(max_groups):
|
| 496 |
-
if i < len(group_keys):
|
| 497 |
-
group = group_keys[i]
|
| 498 |
-
choices = []
|
| 499 |
-
# Collect GET endpoints for this group
|
| 500 |
-
for ep, methods in groups[group].items():
|
| 501 |
-
if 'get' in methods:
|
| 502 |
-
summary = methods['get'].get('summary', 'No summary')
|
| 503 |
-
choices.append(f"{ep} | GET - {summary}")
|
| 504 |
-
|
| 505 |
-
# Add updates for this accordion
|
| 506 |
-
if choices:
|
| 507 |
-
updates.extend([
|
| 508 |
-
gr.update(
|
| 509 |
-
label=f"Group: {group}",
|
| 510 |
-
visible=True,
|
| 511 |
-
open=False # Don't auto-open any accordion
|
| 512 |
-
),
|
| 513 |
-
gr.update(
|
| 514 |
-
choices=choices,
|
| 515 |
-
value=[],
|
| 516 |
-
visible=True,
|
| 517 |
-
interactive=True # Make sure checkboxes are interactive
|
| 518 |
-
)
|
| 519 |
-
])
|
| 520 |
-
else:
|
| 521 |
-
updates.extend([
|
| 522 |
-
gr.update(visible=False, label=""),
|
| 523 |
-
gr.update(visible=False, choices=[], value=[])
|
| 524 |
-
])
|
| 525 |
-
else:
|
| 526 |
-
# Hide unused accordions
|
| 527 |
-
updates.extend([
|
| 528 |
-
gr.update(visible=False, label=""),
|
| 529 |
-
gr.update(visible=False, choices=[], value=[])
|
| 530 |
-
])
|
| 531 |
-
|
| 532 |
-
yield (
|
| 533 |
-
gr.update(visible=False), # hide loading
|
| 534 |
-
gr.update(value="✅ Endpoints loaded successfully", visible=True),
|
| 535 |
-
gr.update(visible=True),
|
| 536 |
-
gr.update(visible=True),
|
| 537 |
-
*updates
|
| 538 |
-
)
|
| 539 |
-
|
| 540 |
-
except Exception as e:
|
| 541 |
yield (
|
| 542 |
-
gr.update(visible=False),
|
| 543 |
-
gr.update(value=
|
| 544 |
gr.update(visible=False),
|
| 545 |
gr.update(visible=False),
|
| 546 |
*[gr.update(visible=False, label="", value=[]) for _ in range(max_groups * 2)]
|
| 547 |
)
|
|
|
|
| 548 |
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
|
| 563 |
-
def lock_selected_endpoints(*checkbox_values):
|
| 564 |
-
"""Collect and lock all selected endpoints"""
|
| 565 |
-
# Show loading message first
|
| 566 |
yield (
|
| 567 |
-
gr.update(
|
| 568 |
-
|
| 569 |
-
gr.update(
|
| 570 |
-
gr.update(
|
| 571 |
-
*
|
| 572 |
-
gr.update(visible=False), # param_group
|
| 573 |
-
gr.update(visible=False), # param_header
|
| 574 |
-
*[gr.update(visible=False) for _ in range(len(param_components) * 3)], # parameter components
|
| 575 |
-
gr.update(interactive=False) # fetch button
|
| 576 |
)
|
| 577 |
|
| 578 |
-
|
| 579 |
-
for
|
| 580 |
-
|
| 581 |
-
all_selected.extend(checkbox_group)
|
| 582 |
|
|
|
|
| 583 |
if not all_selected:
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
gr.update(interactive=
|
| 588 |
-
gr.update(interactive=
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
gr.update(
|
| 593 |
-
gr.update(
|
| 594 |
-
*[gr.update(visible=False) for _ in range(len(param_components) * 3)] # parameter components
|
| 595 |
]
|
| 596 |
-
yield base_updates + param_updates + [gr.update(interactive=False)] # fetch button
|
| 597 |
-
return
|
| 598 |
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
*[gr.update(interactive=False) for _ in range(len(checkbox_values))] # checkboxes
|
| 605 |
-
]
|
| 606 |
-
|
| 607 |
-
param_updates = update_params(spec_choice.value, all_selected)
|
| 608 |
-
fetch_update = [gr.update(interactive=True)]
|
| 609 |
|
| 610 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 611 |
|
| 612 |
-
def
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 616 |
|
| 617 |
-
|
| 618 |
-
endpoints = get_endpoints(
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
all_parameters = [] # This will store ALL parameters from ALL endpoints
|
| 622 |
-
required_params_count = 0
|
| 623 |
-
|
| 624 |
-
# Collect parameters from all endpoints first
|
| 625 |
-
for ep in locked_endpoints:
|
| 626 |
endpoint = ep.split(" | GET")[0].strip()
|
| 627 |
endpoint_spec = endpoints.get(endpoint, {}).get('get', {})
|
| 628 |
-
|
| 629 |
-
# Collect path parameters
|
| 630 |
path_params = extract_path_params(endpoint)
|
| 631 |
-
for param in path_params:
|
| 632 |
-
all_parameters.append({
|
| 633 |
-
'endpoint': endpoint,
|
| 634 |
-
'type': 'path',
|
| 635 |
-
'name': param,
|
| 636 |
-
'required': True,
|
| 637 |
-
'description': f"Required path parameter for {endpoint}"
|
| 638 |
-
})
|
| 639 |
-
required_params_count += 1
|
| 640 |
-
|
| 641 |
-
# Collect required query parameters
|
| 642 |
query_params = extract_query_params(endpoint_spec)
|
| 643 |
-
for
|
|
|
|
|
|
|
| 644 |
if required:
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
'type': 'query',
|
| 648 |
-
'name': name,
|
| 649 |
-
'required': True,
|
| 650 |
-
'description': description
|
| 651 |
-
})
|
| 652 |
-
required_params_count += 1
|
| 653 |
-
|
| 654 |
-
# Create updates for components
|
| 655 |
-
updates = []
|
| 656 |
|
| 657 |
-
|
|
|
|
|
|
|
|
|
|
| 658 |
updates.extend([
|
| 659 |
-
gr.update(visible=
|
| 660 |
-
gr.update(value="✅ No parameters required.
|
| 661 |
])
|
| 662 |
-
updates.extend([gr.update(visible=False)
|
| 663 |
else:
|
| 664 |
-
# Show parameter section
|
| 665 |
updates.extend([
|
| 666 |
-
gr.update(visible=True),
|
| 667 |
-
gr.update(value=f"⚠️ Required Parameters ({
|
| 668 |
])
|
| 669 |
-
|
| 670 |
-
# Update parameter components (limited to first 5 parameters)
|
| 671 |
for i in range(5):
|
| 672 |
if i < len(all_parameters):
|
| 673 |
-
param = all_parameters[i]
|
| 674 |
emoji = "🔑" if param['type'] == 'path' else "🔍"
|
| 675 |
updates.extend([
|
| 676 |
-
gr.update(visible=True),
|
| 677 |
-
gr.update(
|
| 678 |
-
|
| 679 |
-
value=f"Endpoint: {param['endpoint']} - {param['type'].title()} Parameter"
|
| 680 |
-
), # display
|
| 681 |
-
gr.update(
|
| 682 |
-
visible=True,
|
| 683 |
-
label=f"{emoji} Enter {param['type']} parameter: {param['name']}",
|
| 684 |
-
placeholder=param['description']
|
| 685 |
-
) # input
|
| 686 |
])
|
| 687 |
else:
|
| 688 |
-
|
| 689 |
-
updates.extend([
|
| 690 |
-
gr.update(visible=False),
|
| 691 |
-
gr.update(visible=False, value=""),
|
| 692 |
-
gr.update(visible=False, label="")
|
| 693 |
-
])
|
| 694 |
-
|
| 695 |
return updates
|
| 696 |
-
|
| 697 |
-
def update_fetch_button(*param_values):
|
| 698 |
-
|
| 699 |
-
if all(val.strip() for val in param_values if val is not None):
|
| 700 |
return gr.update(interactive=True)
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
for _, _, input_box in param_components:
|
| 704 |
-
input_box.change(
|
| 705 |
-
fn=update_fetch_button,
|
| 706 |
-
inputs=[input_box for _, _, input_box in param_components],
|
| 707 |
-
outputs=[fetch_btn]
|
| 708 |
-
)
|
| 709 |
|
| 710 |
-
def handle_api_call(
|
| 711 |
-
|
| 712 |
-
"
|
| 713 |
-
|
| 714 |
yield (
|
| 715 |
-
gr.update(
|
| 716 |
-
gr.update(visible=False),
|
| 717 |
-
gr.update(value=None),
|
| 718 |
-
gr.update(value=None)
|
| 719 |
)
|
| 720 |
|
| 721 |
-
|
| 722 |
-
yield (
|
| 723 |
-
gr.update(visible=False), # hide loading
|
| 724 |
-
gr.update(visible=True),
|
| 725 |
-
{"error": "No endpoints selected"},
|
| 726 |
-
None
|
| 727 |
-
)
|
| 728 |
-
return
|
| 729 |
-
|
| 730 |
-
# Initialize parameter dictionaries
|
| 731 |
-
path_params = {}
|
| 732 |
-
query_params = {}
|
| 733 |
param_idx = 0
|
| 734 |
-
endpoints = get_endpoints(
|
| 735 |
-
|
| 736 |
-
# Process each endpoint and map parameters
|
| 737 |
for ep in locked_endpoints:
|
| 738 |
-
endpoint = ep.split(" | GET")[0].strip()
|
| 739 |
endpoint_spec = endpoints.get(endpoint, {}).get('get', {})
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
for param in path_params_list:
|
| 744 |
-
if param_idx < len(param_values) and param_values[param_idx]:
|
| 745 |
-
path_params[param] = param_values[param_idx]
|
| 746 |
param_idx += 1
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
for _, name, required, _ in query_params_list:
|
| 751 |
-
if required and param_idx < len(param_values) and param_values[param_idx]:
|
| 752 |
-
query_params[name] = param_values[param_idx]
|
| 753 |
param_idx += 1
|
| 754 |
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
result = handle_okta_call(
|
| 759 |
-
api_base_url,
|
| 760 |
-
api_token,
|
| 761 |
-
"", # session_id
|
| 762 |
-
path_params,
|
| 763 |
-
query_params,
|
| 764 |
-
locked_endpoints
|
| 765 |
-
)
|
| 766 |
-
elif spec_choice == "SailPoint IdentityNow (YAML)":
|
| 767 |
-
result = handle_identitynow_call(
|
| 768 |
-
api_base_url,
|
| 769 |
-
grant_type,
|
| 770 |
-
client_id,
|
| 771 |
-
client_secret,
|
| 772 |
-
"", # session_id
|
| 773 |
-
path_params,
|
| 774 |
-
query_params,
|
| 775 |
-
locked_endpoints
|
| 776 |
-
)
|
| 777 |
-
else: # Sailpoint IIQ
|
| 778 |
-
result = handle_iiq_call(
|
| 779 |
-
api_base_url,
|
| 780 |
-
iiq_username,
|
| 781 |
-
iiq_password,
|
| 782 |
-
"", # session_id
|
| 783 |
-
path_params,
|
| 784 |
-
query_params,
|
| 785 |
-
locked_endpoints
|
| 786 |
-
)
|
| 787 |
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
)
|
| 795 |
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 803 |
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
api_base_url,
|
| 811 |
-
grant_type,
|
| 812 |
-
client_id,
|
| 813 |
-
client_secret,
|
| 814 |
-
api_token,
|
| 815 |
-
iiq_username,
|
| 816 |
-
iiq_password
|
| 817 |
-
],
|
| 818 |
-
outputs=[
|
| 819 |
-
loading_indicator, # Add loading indicator to outputs
|
| 820 |
-
credentials_message,
|
| 821 |
-
endpoints_section,
|
| 822 |
-
fetch_section,
|
| 823 |
-
*[comp for acc_cb in accordion_placeholders for comp in acc_cb]
|
| 824 |
-
]
|
| 825 |
-
)
|
| 826 |
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
|
| 831 |
-
|
| 832 |
-
okta_auth,
|
| 833 |
-
iiq_auth,
|
| 834 |
-
endpoints_section, # sections
|
| 835 |
-
fetch_section,
|
| 836 |
-
results_section,
|
| 837 |
-
results_out, # results
|
| 838 |
-
download_out,
|
| 839 |
-
credentials_message # message
|
| 840 |
-
]
|
| 841 |
)
|
| 842 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 843 |
|
| 844 |
-
|
| 845 |
fn=lock_selected_endpoints,
|
| 846 |
inputs=[cb for _, cb in accordion_placeholders],
|
| 847 |
-
outputs=[
|
| 848 |
-
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
|
| 852 |
-
*[cb for _, cb in accordion_placeholders],
|
| 853 |
-
param_group,
|
| 854 |
-
param_header,
|
| 855 |
-
*[comp for group, display, input_box in param_components
|
| 856 |
-
for comp in (group, display, input_box)],
|
| 857 |
-
fetch_btn
|
| 858 |
-
]
|
| 859 |
)
|
| 860 |
|
| 861 |
-
|
| 862 |
fn=unlock_selected_endpoints,
|
| 863 |
inputs=[],
|
| 864 |
-
outputs=[
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
|
| 869 |
-
*[cb for _, cb in accordion_placeholders],
|
| 870 |
-
param_group,
|
| 871 |
-
param_header,
|
| 872 |
-
*[comp for group, display, input_box in param_components
|
| 873 |
-
for comp in (group, display, input_box)],
|
| 874 |
-
fetch_btn
|
| 875 |
-
]
|
| 876 |
)
|
| 877 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 878 |
fetch_btn.click(
|
| 879 |
fn=handle_api_call,
|
| 880 |
-
inputs=[
|
| 881 |
-
|
| 882 |
-
|
| 883 |
-
grant_type,
|
| 884 |
-
client_id,
|
| 885 |
-
client_secret,
|
| 886 |
-
api_token,
|
| 887 |
-
iiq_username,
|
| 888 |
-
iiq_password,
|
| 889 |
-
locked_endpoints_state,
|
| 890 |
-
*[input_box for _, _, input_box in param_components]
|
| 891 |
-
],
|
| 892 |
-
outputs=[
|
| 893 |
-
fetch_loading_indicator, # Add loading indicator
|
| 894 |
-
results_section,
|
| 895 |
-
results_out,
|
| 896 |
-
download_out
|
| 897 |
-
]
|
| 898 |
)
|
| 899 |
|
| 900 |
if __name__ == "__main__":
|
|
|
|
| 14 |
env_path = script_dir / '.env'
|
| 15 |
load_dotenv(dotenv_path=env_path)
|
| 16 |
|
| 17 |
+
# Helper functions (unchanged)
|
|
|
|
|
|
|
|
|
|
| 18 |
def fetch_api_endpoints_yaml(spec_url):
|
| 19 |
try:
|
| 20 |
response = requests.get(spec_url)
|
|
|
|
| 36 |
if not methods or not isinstance(methods, dict):
|
| 37 |
continue
|
| 38 |
|
|
|
|
| 39 |
common_params = methods.get("parameters", [])
|
|
|
|
| 40 |
for method, details in methods.items():
|
| 41 |
if method.lower() not in valid_methods:
|
| 42 |
continue
|
| 43 |
|
|
|
|
| 44 |
method_params = details.get("parameters", [])
|
| 45 |
all_params = common_params + method_params
|
|
|
|
| 46 |
endpoint_info = {
|
| 47 |
"summary": details.get("summary", ""),
|
| 48 |
"description": details.get("description", ""),
|
| 49 |
+
"parameters": all_params
|
| 50 |
}
|
| 51 |
endpoints[path][method.lower()] = endpoint_info
|
| 52 |
return endpoints
|
|
|
|
| 71 |
if not methods or not isinstance(methods, dict):
|
| 72 |
continue
|
| 73 |
|
|
|
|
| 74 |
common_params = methods.get("parameters", [])
|
|
|
|
| 75 |
for method, details in methods.items():
|
| 76 |
if method.lower() not in valid_methods:
|
| 77 |
continue
|
| 78 |
|
|
|
|
| 79 |
method_params = details.get("parameters", [])
|
| 80 |
all_params = common_params + method_params
|
|
|
|
| 81 |
endpoint_info = {
|
| 82 |
"summary": details.get("summary", ""),
|
| 83 |
"description": details.get("description", ""),
|
| 84 |
+
"parameters": all_params
|
| 85 |
}
|
| 86 |
endpoints[path][method.lower()] = endpoint_info
|
| 87 |
return endpoints
|
|
|
|
| 100 |
return fetch_api_endpoints_yaml(spec_url)
|
| 101 |
|
| 102 |
def group_endpoints(endpoints, spec_choice):
|
|
|
|
| 103 |
groups = {}
|
|
|
|
|
|
|
| 104 |
if spec_choice == "Okta (JSON)":
|
| 105 |
for path, methods in endpoints.items():
|
|
|
|
| 106 |
clean_path = path.replace('/api/v1/', '')
|
| 107 |
segments = clean_path.strip("/").split("/")
|
| 108 |
group_key = segments[0] if segments else "other"
|
|
|
|
| 109 |
if group_key not in groups:
|
| 110 |
groups[group_key] = {}
|
| 111 |
groups[group_key][path] = methods
|
| 112 |
else:
|
|
|
|
| 113 |
for path, methods in endpoints.items():
|
| 114 |
segments = path.strip("/").split("/")
|
| 115 |
group_key = segments[0] if segments[0] != "" else "other"
|
| 116 |
if group_key not in groups:
|
| 117 |
groups[group_key] = {}
|
| 118 |
groups[group_key][path] = methods
|
|
|
|
| 119 |
return groups
|
| 120 |
|
| 121 |
def verify_credentials(spec_choice, api_base_url, grant_type=None, client_id=None,
|
| 122 |
client_secret=None, api_token=None, iiq_username=None, iiq_password=None):
|
|
|
|
|
|
|
|
|
|
| 123 |
if not api_base_url or not api_base_url.strip():
|
| 124 |
return (
|
| 125 |
+
gr.update(value="<div class='error-message'><i class='fas fa-exclamation-circle'></i> API Base URL is required</div>", visible=True),
|
| 126 |
+
False,
|
| 127 |
+
False
|
| 128 |
)
|
| 129 |
|
| 130 |
try:
|
|
|
|
| 131 |
if spec_choice == "Okta (JSON)":
|
| 132 |
if not api_token or not api_token.strip():
|
| 133 |
return (
|
| 134 |
+
gr.update(value="<div class='error-message'><i class='fas fa-exclamation-circle'></i> API Token is required</div>", visible=True),
|
| 135 |
False,
|
| 136 |
False
|
| 137 |
)
|
|
|
|
|
|
|
| 138 |
result = handle_okta_call(api_base_url, api_token, "", {}, ["/api/v1/users/me"])
|
| 139 |
+
|
| 140 |
+
# Check the response for the specific endpoint
|
| 141 |
+
if isinstance(result[0], dict) and "/api/v1/users/me" in result[0]:
|
| 142 |
+
response = result[0]["/api/v1/users/me"]
|
| 143 |
+
|
| 144 |
+
# Check if response is an error string
|
| 145 |
+
if isinstance(response, str) and response.startswith("Error:"):
|
| 146 |
+
return (
|
| 147 |
+
gr.update(value=f"<div class='error-message'><i class='fas fa-exclamation-circle'></i> Connection failed: {response}</div>", visible=True),
|
| 148 |
+
False,
|
| 149 |
+
False
|
| 150 |
+
)
|
| 151 |
+
|
| 152 |
+
# Check if response is an error dict
|
| 153 |
+
if isinstance(response, dict) and "error" in response:
|
| 154 |
+
return (
|
| 155 |
+
gr.update(value=f"<div class='error-message'><i class='fas fa-exclamation-circle'></i> Connection failed: {response['error']}</div>", visible=True),
|
| 156 |
+
False,
|
| 157 |
+
False
|
| 158 |
+
)
|
| 159 |
+
else:
|
| 160 |
+
# Unexpected response format
|
| 161 |
+
return (
|
| 162 |
+
gr.update(value="<div class='error-message'><i class='fas fa-exclamation-circle'></i> Invalid API response format from Okta</div>", visible=True),
|
| 163 |
+
False,
|
| 164 |
+
False
|
| 165 |
+
)
|
| 166 |
|
| 167 |
elif spec_choice == "SailPoint IdentityNow (YAML)":
|
| 168 |
+
if not all([grant_type and grant_type.strip(), client_id and client_id.strip(), client_secret and client_secret.strip()]):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
return (
|
| 170 |
+
gr.update(value="<div class='error-message'><i class='fas fa-exclamation-circle'></i> All IdentityNow credentials are required</div>", visible=True),
|
| 171 |
False,
|
| 172 |
False
|
| 173 |
)
|
| 174 |
+
result = handle_identitynow_call(api_base_url, grant_type, client_id, client_secret, "", {}, ["/beta/test-connection"])
|
| 175 |
|
| 176 |
+
# Check the response
|
| 177 |
+
if isinstance(result[0], dict):
|
| 178 |
+
# Check for direct error in response
|
| 179 |
+
if "error" in result[0]:
|
| 180 |
+
return (
|
| 181 |
+
gr.update(value=f"<div class='error-message'><i class='fas fa-exclamation-circle'></i> Connection failed: {result[0]['error']}</div>", visible=True),
|
| 182 |
+
False,
|
| 183 |
+
False
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
# Check for endpoint-specific error
|
| 187 |
+
if "/beta/test-connection" in result[0]:
|
| 188 |
+
response = result[0]["/beta/test-connection"]
|
| 189 |
+
if isinstance(response, str) and response.startswith("Error:"):
|
| 190 |
+
return (
|
| 191 |
+
gr.update(value=f"<div class='error-message'><i class='fas fa-exclamation-circle'></i> Connection failed: {response}</div>", visible=True),
|
| 192 |
+
False,
|
| 193 |
+
False
|
| 194 |
+
)
|
| 195 |
+
if isinstance(response, dict) and "error" in response:
|
| 196 |
+
return (
|
| 197 |
+
gr.update(value=f"<div class='error-message'><i class='fas fa-exclamation-circle'></i> Connection failed: {response['error']}</div>", visible=True),
|
| 198 |
+
False,
|
| 199 |
+
False
|
| 200 |
+
)
|
| 201 |
+
else:
|
| 202 |
+
return (
|
| 203 |
+
gr.update(value="<div class='error-message'><i class='fas fa-exclamation-circle'></i> Invalid API response format from IdentityNow</div>", visible=True),
|
| 204 |
+
False,
|
| 205 |
+
False
|
| 206 |
+
)
|
| 207 |
|
| 208 |
elif spec_choice == "Sailpoint IIQ (YAML)":
|
| 209 |
+
if not all([iiq_username and iiq_username.strip(), iiq_password and iiq_password.strip()]):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
return (
|
| 211 |
+
gr.update(value="<div class='error-message'><i class='fas fa-exclamation-circle'></i> Both IIQ username and password are required</div>", visible=True),
|
| 212 |
False,
|
| 213 |
False
|
| 214 |
)
|
| 215 |
+
result = handle_iiq_call(api_base_url, iiq_username, iiq_password, "", {}, ["/ping"])
|
| 216 |
|
| 217 |
+
# Check the response
|
| 218 |
+
if isinstance(result[0], dict):
|
| 219 |
+
# Check for direct error in response
|
| 220 |
+
if "error" in result[0]:
|
| 221 |
+
return (
|
| 222 |
+
gr.update(value=f"<div class='error-message'><i class='fas fa-exclamation-circle'></i> Connection failed: {result[0]['error']}</div>", visible=True),
|
| 223 |
+
False,
|
| 224 |
+
False
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
# Check for endpoint-specific error
|
| 228 |
+
if "/ping" in result[0]:
|
| 229 |
+
response = result[0]["/ping"]
|
| 230 |
+
if isinstance(response, str) and response.startswith("Error:"):
|
| 231 |
+
return (
|
| 232 |
+
gr.update(value=f"<div class='error-message'><i class='fas fa-exclamation-circle'></i> Connection failed: {response}</div>", visible=True),
|
| 233 |
+
False,
|
| 234 |
+
False
|
| 235 |
+
)
|
| 236 |
+
if isinstance(response, dict) and "error" in response:
|
| 237 |
+
return (
|
| 238 |
+
gr.update(value=f"<div class='error-message'><i class='fas fa-exclamation-circle'></i> Connection failed: {response['error']}</div>", visible=True),
|
| 239 |
+
False,
|
| 240 |
+
False
|
| 241 |
+
)
|
| 242 |
+
else:
|
| 243 |
+
return (
|
| 244 |
+
gr.update(value="<div class='error-message'><i class='fas fa-exclamation-circle'></i> Invalid API response format from IIQ</div>", visible=True),
|
| 245 |
+
False,
|
| 246 |
+
False
|
| 247 |
+
)
|
| 248 |
|
| 249 |
+
# If no error is detected and response is valid, assume success
|
| 250 |
return (
|
| 251 |
+
gr.update(value="<div class='success-message'><i class='fas fa-check-circle'></i> Credentials verified successfully!</div>", visible=True),
|
| 252 |
+
True,
|
| 253 |
+
True
|
| 254 |
)
|
| 255 |
|
| 256 |
except requests.exceptions.ConnectionError:
|
| 257 |
return (
|
| 258 |
+
gr.update(value="<div class='error-message'><i class='fas fa-exclamation-circle'></i> Could not reach the API. Check URL and network.</div>", visible=True),
|
| 259 |
False,
|
| 260 |
False
|
| 261 |
)
|
| 262 |
except Exception as e:
|
| 263 |
return (
|
| 264 |
+
gr.update(value=f"<div class='error-message'><i class='fas fa-exclamation-circle'></i> API Error: {str(e)}</div>", visible=True),
|
| 265 |
False,
|
| 266 |
False
|
| 267 |
)
|
| 268 |
|
| 269 |
+
# Custom CSS (unchanged)
|
| 270 |
custom_css = """
|
| 271 |
+
@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css');
|
| 272 |
+
|
| 273 |
+
body, .gradio-container {
|
| 274 |
+
font-family: 'Arial', sans-serif;
|
| 275 |
+
background: #f3f3f3;
|
| 276 |
+
color: #333;
|
| 277 |
+
margin: 0;
|
| 278 |
+
padding: 0;
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
.title {
|
| 282 |
+
text-align: center;
|
| 283 |
+
margin: 30px 0;
|
| 284 |
+
font-size: 2em;
|
| 285 |
+
color: #222;
|
| 286 |
+
animation: fadeIn 1s ease-in;
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
.api-cards {
|
| 290 |
+
display: flex;
|
| 291 |
+
justify-content: center;
|
| 292 |
+
gap: 20px;
|
| 293 |
+
margin-bottom: 30px;
|
| 294 |
+
animation: slideUp 0.5s ease-out;
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
.api-card {
|
| 298 |
+
background: white;
|
| 299 |
+
padding: 20px;
|
| 300 |
+
border-radius: 10px;
|
| 301 |
+
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
| 302 |
+
text-align: center;
|
| 303 |
+
cursor: pointer;
|
| 304 |
+
transition: transform 0.2s, box-shadow 0.2s;
|
| 305 |
+
width: 200px;
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
.api-card:hover {
|
| 309 |
+
transform: translateY(-5px);
|
| 310 |
+
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
.api-card i {
|
| 314 |
+
font-size: 32px;
|
| 315 |
+
color: #007BFF;
|
| 316 |
+
margin-bottom: 10px;
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
.api-card h2 {
|
| 320 |
+
font-size: 18px;
|
| 321 |
+
margin: 10px 0;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
.api-card p {
|
| 325 |
+
font-size: 14px;
|
| 326 |
+
color: #666;
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
.cred-section {
|
| 330 |
+
background: white;
|
| 331 |
+
padding: 20px;
|
| 332 |
+
border-radius: 10px;
|
| 333 |
+
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
| 334 |
+
margin-bottom: 20px;
|
| 335 |
+
animation: fadeIn 0.5s ease-in;
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
.action-btn {
|
| 339 |
+
background-color: #007BFF;
|
| 340 |
+
color: white;
|
| 341 |
+
border: none;
|
| 342 |
+
padding: 10px 20px;
|
| 343 |
+
border-radius: 5px;
|
| 344 |
+
cursor: pointer;
|
| 345 |
+
transition: background-color 0.2s;
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
.action-btn:hover {
|
| 349 |
+
background-color: #0056b3;
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
.loading-spinner {
|
| 353 |
+
border: 4px solid #f3f3f3;
|
| 354 |
+
border-top: 4px solid #3498db;
|
| 355 |
+
border-radius: 50%;
|
| 356 |
+
width: 30px;
|
| 357 |
+
height: 30px;
|
| 358 |
+
animation: spin 1s linear infinite;
|
| 359 |
+
margin: 10px auto;
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
@keyframes spin {
|
| 363 |
+
0% { transform: rotate(0deg); }
|
| 364 |
+
100% { transform: rotate(360deg); }
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
.success-message, .error-message {
|
| 368 |
+
padding: 10px;
|
| 369 |
+
border-radius: 5px;
|
| 370 |
+
margin-top: 10px;
|
| 371 |
+
display: flex;
|
| 372 |
+
align-items: center;
|
| 373 |
+
gap: 10px;
|
| 374 |
+
animation: slideIn 0.3s ease-out;
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
.success-message {
|
| 378 |
+
background: #d4edda;
|
| 379 |
+
color: #155724;
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
.error-message {
|
| 383 |
+
background: #f8d7da;
|
| 384 |
+
color: #721c24;
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
.endpoints-section, .param-group, .fetch-section, .results-section {
|
| 388 |
+
background: white;
|
| 389 |
+
padding: 20px;
|
| 390 |
+
border-radius: 10px;
|
| 391 |
+
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
| 392 |
+
margin-bottom: 20px;
|
| 393 |
+
animation: fadeIn 0.5s ease-in;
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
.custom-accordion {
|
| 397 |
+
border: 1px solid #ddd;
|
| 398 |
+
border-radius: 5px;
|
| 399 |
+
margin-bottom: 10px;
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
.custom-accordion .gr-accordion-header {
|
| 403 |
+
background: #f9f9f9;
|
| 404 |
+
padding: 10px;
|
| 405 |
+
cursor: pointer;
|
| 406 |
+
font-weight: bold;
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
.custom-checkbox .gr-checkbox-group {
|
| 410 |
+
display: flex;
|
| 411 |
+
flex-direction: column;
|
| 412 |
+
gap: 5px;
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
.reset-btn {
|
| 416 |
+
background-color: #6c757d;
|
| 417 |
+
color: white;
|
| 418 |
+
border: none;
|
| 419 |
+
padding: 8px 16px;
|
| 420 |
+
border-radius: 5px;
|
| 421 |
+
cursor: pointer;
|
| 422 |
+
transition: background-color 0.2s;
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
.reset-btn:hover:enabled {
|
| 426 |
+
background-color: #5a6268;
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
.gr-json {
|
| 430 |
+
background: #f9f9f9;
|
| 431 |
+
padding: 10px;
|
| 432 |
+
border: 1px solid #ddd;
|
| 433 |
+
border-radius: 5px;
|
| 434 |
+
max-height: 400px;
|
| 435 |
+
overflow: auto;
|
| 436 |
+
font-family: monospace;
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
.download-btn {
|
| 440 |
+
background-color: #28a745;
|
| 441 |
+
color: white;
|
| 442 |
+
border: none;
|
| 443 |
+
padding: 10px 20px;
|
| 444 |
+
border-radius: 5px;
|
| 445 |
+
cursor: pointer;
|
| 446 |
+
transition: background-color 0.2s;
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
.download-btn:hover {
|
| 450 |
+
background-color: #218838;
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
@keyframes fadeIn {
|
| 454 |
+
from { opacity: 0; }
|
| 455 |
+
to { opacity: 1; }
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
@keyframes slideUp {
|
| 459 |
+
from { transform: translateY(20px); opacity: 0; }
|
| 460 |
+
to { transform: translateY(0); opacity: 1; }
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
@keyframes slideIn {
|
| 464 |
+
from { transform: translateX(-20px); opacity: 0; }
|
| 465 |
+
to { transform: translateX(0); opacity: 1; }
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
.banner {
|
| 469 |
+
background-color: #007BFF;
|
| 470 |
+
color: white;
|
| 471 |
+
padding: 20px;
|
| 472 |
+
text-align: center;
|
| 473 |
+
font-size: 24px;
|
| 474 |
+
font-weight: bold;
|
| 475 |
+
margin-bottom: 20px;
|
| 476 |
+
}
|
| 477 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 478 |
|
| 479 |
with gr.Blocks(css=custom_css) as demo:
|
| 480 |
+
gr.HTML("<div class='banner'>Data Connector Demo</div>")
|
| 481 |
|
| 482 |
+
# Session states
|
| 483 |
session_id_state = gr.State("")
|
|
|
|
|
|
|
| 484 |
locked_endpoints_state = gr.State([])
|
| 485 |
+
required_params_count = gr.State(0)
|
| 486 |
+
|
| 487 |
+
# API Selection
|
| 488 |
+
# Define API options as a list of dictionaries
|
| 489 |
+
api_options = [
|
| 490 |
+
{"icon": "fa-cloud", "title": "Okta", "description": "Identity and Access Management", "value": "Okta (JSON)"},
|
| 491 |
+
{"icon": "fa-user-shield", "title": "SailPoint IdentityNow", "description": "Cloud Identity Governance", "value": "SailPoint IdentityNow (YAML)"},
|
| 492 |
+
{"icon": "fa-network-wired", "title": "Sailpoint IIQ", "description": "On-premise Identity Governance", "value": "Sailpoint IIQ (YAML)"},
|
| 493 |
+
]
|
| 494 |
+
|
| 495 |
+
# Generate choices with plain text labels and corresponding values
|
| 496 |
+
choices = [(opt["title"], opt["value"]) for opt in api_options]
|
| 497 |
+
|
| 498 |
+
# Define the radio button component
|
| 499 |
+
api_choice = gr.Radio(
|
| 500 |
+
choices=choices,
|
| 501 |
+
label="",
|
| 502 |
+
type="value",
|
| 503 |
+
elem_classes="api-cards"
|
| 504 |
+
)
|
| 505 |
|
| 506 |
+
# Credential Sections
|
| 507 |
+
with gr.Column(visible=False, elem_classes="cred-section") as okta_cred:
|
| 508 |
+
api_base_url_okta = gr.Textbox(label="API Base URL", placeholder="e.g., https://your-okta-domain.okta.com")
|
| 509 |
+
api_token = gr.Textbox(label="API Token", type="password", placeholder="Enter your Okta API token")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 510 |
|
| 511 |
+
with gr.Column(visible=False, elem_classes="cred-section") as inow_cred:
|
| 512 |
+
api_base_url_inow = gr.Textbox(label="API Base URL", placeholder="e.g., https://your-org.api.identitynow.com")
|
| 513 |
+
grant_type = gr.Textbox(label="Grant Type", value="client_credentials", interactive=False)
|
| 514 |
+
client_id = gr.Textbox(label="Client ID", placeholder="Enter your client ID")
|
| 515 |
+
client_secret = gr.Textbox(label="Client Secret", type="password", placeholder="Enter your client secret")
|
| 516 |
+
|
| 517 |
+
with gr.Column(visible=False, elem_classes="cred-section") as iiq_cred:
|
| 518 |
+
api_base_url_iiq = gr.Textbox(label="API Base URL", placeholder="e.g., https://your-iiq-server.com/identityiq")
|
| 519 |
+
username = gr.Textbox(label="Username", placeholder="Enter your IIQ username")
|
| 520 |
+
password = gr.Textbox(label="Password", type="password", placeholder="Enter your IIQ password")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 521 |
|
| 522 |
+
verify_btn = gr.Button("Verify Credentials", elem_classes="action-btn")
|
| 523 |
+
loading_indicator = gr.HTML("<div class='loading-spinner'></div>", visible=False)
|
| 524 |
+
credentials_message = gr.HTML("", visible=False)
|
| 525 |
+
|
| 526 |
+
# Endpoints Section
|
| 527 |
max_groups = 100
|
| 528 |
+
with gr.Group(elem_classes="endpoints-section", visible=False) as endpoints_section:
|
| 529 |
+
reset_btn = gr.Button("Reset", interactive=False, elem_classes="reset-btn")
|
|
|
|
| 530 |
accordion_placeholders = []
|
| 531 |
+
for i in range(max_groups):
|
| 532 |
+
with gr.Accordion(label="", open=False, visible=False, elem_classes="custom-accordion") as acc:
|
| 533 |
+
cb = gr.CheckboxGroup(label="", choices=[], value=[], elem_classes="custom-checkbox")
|
| 534 |
+
accordion_placeholders.append((acc, cb))
|
| 535 |
+
continue_btn = gr.Button("Continue", elem_classes="action-btn")
|
| 536 |
+
|
| 537 |
+
# Parameter Section
|
| 538 |
+
with gr.Group(elem_classes="param-group", visible=False) as param_group:
|
| 539 |
+
param_header = gr.Markdown("### Parameters Required")
|
| 540 |
+
param_components = []
|
| 541 |
+
for i in range(5):
|
| 542 |
+
with gr.Group(visible=False) as group:
|
| 543 |
+
param_display = gr.Markdown(visible=False)
|
| 544 |
+
param_input = gr.Textbox(label="Parameter Value", visible=False)
|
| 545 |
+
param_components.append((group, param_display, param_input))
|
| 546 |
+
|
| 547 |
+
# Fetch Section
|
| 548 |
+
with gr.Group(elem_classes="fetch-section", visible=False) as fetch_section:
|
| 549 |
+
fetch_btn = gr.Button("Fetch Data", interactive=False, elem_classes="action-btn")
|
| 550 |
+
fetch_loading_indicator = gr.HTML("<div class='loading-spinner'></div>", visible=False)
|
| 551 |
+
|
| 552 |
+
# Results Section
|
| 553 |
+
with gr.Group(elem_classes="results-section", visible=False) as results_section:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 554 |
gr.Markdown("### Results")
|
| 555 |
+
results_out = gr.JSON(label="API Responses", elem_classes="gr-json")
|
| 556 |
+
download_out = gr.File(label="Download Session Data (ZIP)", elem_classes="download-btn")
|
| 557 |
+
|
| 558 |
+
# Event Handlers
|
| 559 |
+
def update_cred_sections(api_choice):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 560 |
return [
|
| 561 |
+
gr.update(visible=api_choice=="Okta (JSON)"),
|
| 562 |
+
gr.update(visible=api_choice=="SailPoint IdentityNow (YAML)"),
|
| 563 |
+
gr.update(visible=api_choice=="Sailpoint IIQ (YAML)"),
|
| 564 |
+
gr.update(visible=False),
|
| 565 |
+
gr.update(visible=False),
|
| 566 |
+
gr.update(visible=False),
|
| 567 |
+
gr.update(value=None),
|
| 568 |
+
gr.update(value=None),
|
| 569 |
+
gr.update(value="")
|
| 570 |
]
|
| 571 |
|
| 572 |
+
def verify_and_load_endpoints(api_choice, api_base_url_okta, api_token, api_base_url_inow, grant_type,
|
| 573 |
+
client_id, client_secret, api_base_url_iiq, username, password):
|
|
|
|
|
|
|
| 574 |
yield (
|
| 575 |
+
gr.update(visible=True),
|
| 576 |
+
gr.update(value=""),
|
| 577 |
+
gr.update(visible=False),
|
| 578 |
+
gr.update(visible=False),
|
| 579 |
+
*[gr.update(visible=False, label="", value=[]) for _ in range(max_groups * 2)]
|
| 580 |
)
|
| 581 |
|
| 582 |
+
if api_choice == "Okta (JSON)":
|
| 583 |
+
status, endpoints_vis, fetch_vis = verify_credentials(api_choice, api_base_url_okta, api_token=api_token)
|
| 584 |
+
elif api_choice == "SailPoint IdentityNow (YAML)":
|
| 585 |
+
status, endpoints_vis, fetch_vis = verify_credentials(api_choice, api_base_url_inow, grant_type=grant_type,
|
| 586 |
+
client_id=client_id, client_secret=client_secret)
|
| 587 |
+
else:
|
| 588 |
+
status, endpoints_vis, fetch_vis = verify_credentials(api_choice, api_base_url_iiq, iiq_username=username,
|
| 589 |
+
iiq_password=password)
|
| 590 |
+
|
| 591 |
if not endpoints_vis:
|
| 592 |
yield (
|
| 593 |
+
gr.update(visible=False),
|
| 594 |
+
status,
|
| 595 |
+
gr.update(visible=False),
|
| 596 |
+
gr.update(visible=False),
|
| 597 |
*[gr.update(visible=False, label="", value=[]) for _ in range(max_groups * 2)]
|
| 598 |
)
|
| 599 |
return
|
| 600 |
+
|
| 601 |
+
endpoints = get_endpoints(api_choice)
|
| 602 |
+
if not endpoints:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 603 |
yield (
|
| 604 |
+
gr.update(visible=False),
|
| 605 |
+
gr.update(value="<div class='error-message'><i class='fas fa-exclamation-circle'></i> No endpoints found</div>", visible=True),
|
| 606 |
gr.update(visible=False),
|
| 607 |
gr.update(visible=False),
|
| 608 |
*[gr.update(visible=False, label="", value=[]) for _ in range(max_groups * 2)]
|
| 609 |
)
|
| 610 |
+
return
|
| 611 |
|
| 612 |
+
groups = group_endpoints(endpoints, api_choice)
|
| 613 |
+
group_keys = list(groups.keys())
|
| 614 |
+
updates = []
|
| 615 |
+
for i in range(max_groups):
|
| 616 |
+
if i < len(group_keys):
|
| 617 |
+
group = group_keys[i]
|
| 618 |
+
choices = [f"{ep} | GET - {methods['get'].get('summary', 'No summary')}" for ep, methods in groups[group].items() if 'get' in methods]
|
| 619 |
+
updates.extend([
|
| 620 |
+
gr.update(label=group, visible=bool(choices), open=False),
|
| 621 |
+
gr.update(choices=choices, value=[], visible=bool(choices), interactive=True)
|
| 622 |
+
])
|
| 623 |
+
else:
|
| 624 |
+
updates.extend([gr.update(visible=False, label=""), gr.update(visible=False, choices=[], value=[])])
|
| 625 |
|
|
|
|
|
|
|
|
|
|
| 626 |
yield (
|
| 627 |
+
gr.update(visible=False),
|
| 628 |
+
gr.update(value="<div class='success-message'><i class='fas fa-check-circle'></i> Endpoints loaded successfully</div>", visible=True),
|
| 629 |
+
gr.update(visible=True),
|
| 630 |
+
gr.update(visible=True),
|
| 631 |
+
*updates
|
|
|
|
|
|
|
|
|
|
|
|
|
| 632 |
)
|
| 633 |
|
| 634 |
+
def lock_selected_endpoints(*checkbox_values):
|
| 635 |
+
all_selected = [sel for group in checkbox_values if isinstance(group, list) for sel in group]
|
| 636 |
+
print("Selected endpoints:", all_selected)
|
|
|
|
| 637 |
|
| 638 |
+
# Handle empty selection case
|
| 639 |
if not all_selected:
|
| 640 |
+
return [
|
| 641 |
+
[], # Raw empty list for locked_endpoints_state
|
| 642 |
+
gr.update(interactive=True),
|
| 643 |
+
gr.update(interactive=False),
|
| 644 |
+
*[gr.update(interactive=True) for _ in range(max_groups)],
|
| 645 |
+
gr.update(visible=False),
|
| 646 |
+
gr.update(visible=False),
|
| 647 |
+
*[gr.update(visible=False) for _ in range(len(param_components) * 3)],
|
| 648 |
+
gr.update(interactive=False),
|
| 649 |
+
gr.update(value=0)
|
|
|
|
| 650 |
]
|
|
|
|
|
|
|
| 651 |
|
| 652 |
+
# Process selected endpoints
|
| 653 |
+
all_parameters = get_required_params(api_choice.value, all_selected)
|
| 654 |
+
param_updates = update_params(api_choice.value, all_selected)
|
| 655 |
+
has_params = len(all_parameters) > 0
|
| 656 |
+
fetch_update = gr.update(interactive=not has_params)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 657 |
|
| 658 |
+
# Create return list with raw endpoints list as first item
|
| 659 |
+
print("About to return with value:", all_selected)
|
| 660 |
+
return [
|
| 661 |
+
all_selected, # Raw list for locked_endpoints_state
|
| 662 |
+
gr.update(interactive=False),
|
| 663 |
+
gr.update(interactive=True),
|
| 664 |
+
*[gr.update(interactive=False) for _ in range(max_groups)],
|
| 665 |
+
*param_updates,
|
| 666 |
+
fetch_update,
|
| 667 |
+
gr.update(value=len(all_parameters))
|
| 668 |
+
]
|
| 669 |
|
| 670 |
+
def unlock_selected_endpoints():
|
| 671 |
+
return [
|
| 672 |
+
gr.update(value=[]),
|
| 673 |
+
gr.update(interactive=True),
|
| 674 |
+
gr.update(interactive=False),
|
| 675 |
+
*[gr.update(interactive=True) for _ in range(max_groups)],
|
| 676 |
+
gr.update(visible=False),
|
| 677 |
+
gr.update(visible=False),
|
| 678 |
+
*[gr.update(visible=False) for _ in range(len(param_components) * 3)],
|
| 679 |
+
gr.update(interactive=False),
|
| 680 |
+
gr.update(value=0)
|
| 681 |
+
]
|
| 682 |
|
| 683 |
+
def get_required_params(spec_choice, selected_endpoints):
|
| 684 |
+
endpoints = get_endpoints(spec_choice)
|
| 685 |
+
all_params = []
|
| 686 |
+
for ep in selected_endpoints:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 687 |
endpoint = ep.split(" | GET")[0].strip()
|
| 688 |
endpoint_spec = endpoints.get(endpoint, {}).get('get', {})
|
|
|
|
|
|
|
| 689 |
path_params = extract_path_params(endpoint)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 690 |
query_params = extract_query_params(endpoint_spec)
|
| 691 |
+
for param in path_params:
|
| 692 |
+
all_params.append({"endpoint": endpoint, "type": "path", "name": param, "description": "Path parameter"})
|
| 693 |
+
for _, name, required, desc in query_params:
|
| 694 |
if required:
|
| 695 |
+
all_params.append({"endpoint": endpoint, "type": "query", "name": name, "description": desc or "Query parameter"})
|
| 696 |
+
return all_params
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 697 |
|
| 698 |
+
def update_params(spec_choice_value, locked_endpoints):
|
| 699 |
+
all_parameters = get_required_params(spec_choice_value, locked_endpoints)
|
| 700 |
+
updates = []
|
| 701 |
+
if not all_parameters:
|
| 702 |
updates.extend([
|
| 703 |
+
gr.update(visible=True),
|
| 704 |
+
gr.update(value="✅ No parameters are required. Press 'Fetch Data' to get data.", visible=True)
|
| 705 |
])
|
| 706 |
+
updates.extend([gr.update(visible=False) for _ in range(len(param_components) * 3)])
|
| 707 |
else:
|
|
|
|
| 708 |
updates.extend([
|
| 709 |
+
gr.update(visible=True),
|
| 710 |
+
gr.update(value=f"⚠️ Required Parameters ({len(all_parameters)})", visible=True)
|
| 711 |
])
|
|
|
|
|
|
|
| 712 |
for i in range(5):
|
| 713 |
if i < len(all_parameters):
|
| 714 |
+
param = all_parameters[i]
|
| 715 |
emoji = "🔑" if param['type'] == 'path' else "🔍"
|
| 716 |
updates.extend([
|
| 717 |
+
gr.update(visible=True),
|
| 718 |
+
gr.update(visible=True, value=f"Endpoint: {param['endpoint']} - {param['type'].title()} Parameter"),
|
| 719 |
+
gr.update(visible=True, label=f"{emoji} {param['name']}", placeholder=param['description'])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 720 |
])
|
| 721 |
else:
|
| 722 |
+
updates.extend([gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 723 |
return updates
|
| 724 |
+
|
| 725 |
+
def update_fetch_button(required_params_count, *param_values):
|
| 726 |
+
if required_params_count == 0:
|
|
|
|
| 727 |
return gr.update(interactive=True)
|
| 728 |
+
filled = all(val and val.strip() for val in param_values[:required_params_count])
|
| 729 |
+
return gr.update(interactive=filled)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 730 |
|
| 731 |
+
def handle_api_call(api_choice, api_base_url_okta, api_token, api_base_url_inow, grant_type, client_id,
|
| 732 |
+
client_secret, api_base_url_iiq, username, password, locked_endpoints, *param_values):
|
| 733 |
+
print("API call received endpoints:", locked_endpoints)
|
|
|
|
| 734 |
yield (
|
| 735 |
+
gr.update(visible=True),
|
| 736 |
+
gr.update(visible=False),
|
| 737 |
+
gr.update(value=None),
|
| 738 |
+
gr.update(value=None)
|
| 739 |
)
|
| 740 |
|
| 741 |
+
param_dict = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 742 |
param_idx = 0
|
| 743 |
+
endpoints = get_endpoints(api_choice)
|
|
|
|
|
|
|
| 744 |
for ep in locked_endpoints:
|
| 745 |
+
endpoint = ep.split(" | GET")[0].strip()
|
| 746 |
endpoint_spec = endpoints.get(endpoint, {}).get('get', {})
|
| 747 |
+
for param in extract_path_params(endpoint):
|
| 748 |
+
if param_idx < len(param_values):
|
| 749 |
+
param_dict[param] = param_values[param_idx]
|
|
|
|
|
|
|
|
|
|
| 750 |
param_idx += 1
|
| 751 |
+
for _, name, required, _ in extract_query_params(endpoint_spec):
|
| 752 |
+
if required and param_idx < len(param_values):
|
| 753 |
+
param_dict[name] = param_values[param_idx]
|
|
|
|
|
|
|
|
|
|
| 754 |
param_idx += 1
|
| 755 |
|
| 756 |
+
# Clean the endpoints to pass only the paths to the API call functions
|
| 757 |
+
clean_endpoints = [ep.split(" | GET")[0].strip() for ep in locked_endpoints]
|
| 758 |
+
print("Selected endpoints to call:", clean_endpoints)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 759 |
|
| 760 |
+
if api_choice == "Okta (JSON)":
|
| 761 |
+
# Pass locked_endpoints as the only item in a list to match expected format
|
| 762 |
+
result = handle_okta_call(api_base_url_okta, api_token, "", param_dict, clean_endpoints)
|
| 763 |
+
elif api_choice == "SailPoint IdentityNow (YAML)":
|
| 764 |
+
result = handle_identitynow_call(api_base_url_inow, grant_type, client_id, client_secret, "", param_dict, clean_endpoints)
|
| 765 |
+
else:
|
| 766 |
+
result = handle_iiq_call(api_base_url_iiq, username, password, "", param_dict, clean_endpoints)
|
| 767 |
|
| 768 |
+
# Process the API result for display
|
| 769 |
+
if not result or not isinstance(result, tuple) or len(result) < 2:
|
| 770 |
+
data = {"error": "Invalid API response format"}
|
| 771 |
+
download_file = None
|
| 772 |
+
else:
|
| 773 |
+
api_data = result[0]
|
| 774 |
+
download_file = result[1] if len(result) > 1 else None
|
| 775 |
+
|
| 776 |
+
if isinstance(api_data, dict):
|
| 777 |
+
data = api_data
|
| 778 |
+
elif isinstance(api_data, list):
|
| 779 |
+
data = {"results": api_data}
|
| 780 |
+
elif api_data is None:
|
| 781 |
+
data = {"message": "No data returned from the API"}
|
| 782 |
+
else:
|
| 783 |
+
data = {"raw_response": str(api_data)}
|
| 784 |
|
| 785 |
+
yield (
|
| 786 |
+
gr.update(visible=False),
|
| 787 |
+
gr.update(visible=True),
|
| 788 |
+
gr.update(value=data),
|
| 789 |
+
gr.update(value=download_file)
|
| 790 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 791 |
|
| 792 |
+
# Wire Events
|
| 793 |
+
api_choice.change(
|
| 794 |
+
fn=update_cred_sections,
|
| 795 |
+
inputs=[api_choice],
|
| 796 |
+
outputs=[okta_cred, inow_cred, iiq_cred, endpoints_section, fetch_section, results_section, results_out, download_out, credentials_message]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 797 |
)
|
| 798 |
|
| 799 |
+
verify_btn.click(
|
| 800 |
+
fn=verify_and_load_endpoints,
|
| 801 |
+
inputs=[api_choice, api_base_url_okta, api_token, api_base_url_inow, grant_type, client_id, client_secret, api_base_url_iiq, username, password],
|
| 802 |
+
outputs=[loading_indicator, credentials_message, endpoints_section, fetch_section] + [comp for acc, cb in accordion_placeholders for comp in (acc, cb)]
|
| 803 |
+
)
|
| 804 |
|
| 805 |
+
continue_btn.click(
|
| 806 |
fn=lock_selected_endpoints,
|
| 807 |
inputs=[cb for _, cb in accordion_placeholders],
|
| 808 |
+
outputs=[locked_endpoints_state, continue_btn, reset_btn] +
|
| 809 |
+
[cb for _, cb in accordion_placeholders] +
|
| 810 |
+
[param_group, param_header] +
|
| 811 |
+
[comp for group, display, input_box in param_components for comp in (group, display, input_box)] +
|
| 812 |
+
[fetch_btn, required_params_count]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 813 |
)
|
| 814 |
|
| 815 |
+
reset_btn.click(
|
| 816 |
fn=unlock_selected_endpoints,
|
| 817 |
inputs=[],
|
| 818 |
+
outputs=[locked_endpoints_state, continue_btn, reset_btn] +
|
| 819 |
+
[cb for _, cb in accordion_placeholders] +
|
| 820 |
+
[param_group, param_header] +
|
| 821 |
+
[comp for group, display, input_box in param_components for comp in (group, display, input_box)] +
|
| 822 |
+
[fetch_btn, required_params_count]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 823 |
)
|
| 824 |
+
|
| 825 |
+
for _, _, input_box in param_components:
|
| 826 |
+
input_box.change(
|
| 827 |
+
fn=update_fetch_button,
|
| 828 |
+
inputs=[required_params_count] + [input_box for _, _, input_box in param_components],
|
| 829 |
+
outputs=[fetch_btn]
|
| 830 |
+
)
|
| 831 |
+
|
| 832 |
fetch_btn.click(
|
| 833 |
fn=handle_api_call,
|
| 834 |
+
inputs=[api_choice, api_base_url_okta, api_token, api_base_url_inow, grant_type, client_id, client_secret, api_base_url_iiq, username, password, locked_endpoints_state] +
|
| 835 |
+
[input_box for _, _, input_box in param_components],
|
| 836 |
+
outputs=[fetch_loading_indicator, results_section, results_out, download_out]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 837 |
)
|
| 838 |
|
| 839 |
if __name__ == "__main__":
|