legate 0.1.0

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.
Files changed (317) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +345 -0
  4. data/bin/legate +13 -0
  5. data/examples/00_quickstart.rb +51 -0
  6. data/examples/01_simple_agent.rb +105 -0
  7. data/examples/02_multi_tool_agent.rb +140 -0
  8. data/examples/03_custom_tool.rb +93 -0
  9. data/examples/04_agent_instructions.rb +84 -0
  10. data/examples/05_state_and_sessions.rb +91 -0
  11. data/examples/06_callbacks.rb +186 -0
  12. data/examples/07_async_jobs.rb +112 -0
  13. data/examples/08_loop_agent.rb +197 -0
  14. data/examples/09_sequential_workflow.rb +40 -0
  15. data/examples/10_parallel_workflow.rb +34 -0
  16. data/examples/11_agent_delegation.rb +24 -0
  17. data/examples/12_http_client_tool.rb +156 -0
  18. data/examples/13_authentication.rb +220 -0
  19. data/examples/14_mcp_client.rb +154 -0
  20. data/examples/15_mcp_server.rb +79 -0
  21. data/examples/16_webhooks.rb +91 -0
  22. data/examples/README_sequential_agents.md +164 -0
  23. data/examples/advanced/auth/cookie_auth_tool.rb +146 -0
  24. data/examples/advanced/auth/custom_auth_flows_example.rb +626 -0
  25. data/examples/advanced/auth/excon_middleware.rb +317 -0
  26. data/examples/advanced/auth/excon_middleware_auth.rb +399 -0
  27. data/examples/advanced/auth/fiber_auth_example.rb +281 -0
  28. data/examples/advanced/auth/fiber_oidc_example.rb +403 -0
  29. data/examples/advanced/auth/httpbin_bearer_tool.rb +159 -0
  30. data/examples/advanced/auth/oauth2_auth.rb +419 -0
  31. data/examples/advanced/auth/oidc_auth.rb +514 -0
  32. data/examples/advanced/auth/openweather_api.rb +251 -0
  33. data/examples/advanced/auth/openweather_tool.rb +153 -0
  34. data/examples/advanced/auth/query_param_middleware_test.rb +138 -0
  35. data/examples/advanced/auth/service_account.rb +135 -0
  36. data/examples/advanced/auth/test_with_httpbin.rb +202 -0
  37. data/examples/advanced/auth/token_lifecycle_example.rb +428 -0
  38. data/examples/advanced/callback_monitoring.rb +679 -0
  39. data/examples/advanced/mas/fixed_delegation_example.rb +191 -0
  40. data/examples/advanced/mas/loop_workflow.rb +28 -0
  41. data/examples/advanced/mas/mock_planner.rb +77 -0
  42. data/examples/advanced/mas/proper_delegation_example.rb +276 -0
  43. data/examples/advanced/mcp/legate_mcp_server_resource_example.rb +182 -0
  44. data/examples/advanced/mcp/mcp_resource_server_example.rb +309 -0
  45. data/examples/advanced/mcp/mcp_server_async.rb +76 -0
  46. data/examples/advanced/mcp/mcp_server_async_tools.rb +122 -0
  47. data/examples/advanced/mcp/mcp_server_legate_agent.rb +95 -0
  48. data/examples/advanced/mcp/mcp_server_rack.rb +89 -0
  49. data/examples/advanced/random_calculator.rb +104 -0
  50. data/examples/advanced/sleep_agent.rb +153 -0
  51. data/examples/advanced/webhooks/webhook_e2e_runner.rb +110 -0
  52. data/examples/advanced/webhooks/webhook_receiver_agent.rb +58 -0
  53. data/examples/advanced/workflows/task_refinement_loop_agent.rb +278 -0
  54. data/examples/advanced/workflows/travel_planner_auto_sequential.rb +444 -0
  55. data/examples/advanced/workflows/travel_planner_parallel.rb +656 -0
  56. data/examples/advanced/workflows/travel_planner_sequential.rb +512 -0
  57. data/examples/tools/oauth2_example.rb +136 -0
  58. data/examples/tools/sleepy_tool.rb +42 -0
  59. data/lib/legate/activity_log.rb +71 -0
  60. data/lib/legate/agent.rb +959 -0
  61. data/lib/legate/agent_code_generator.rb +185 -0
  62. data/lib/legate/agent_definition.rb +812 -0
  63. data/lib/legate/agentic/decision.rb +49 -0
  64. data/lib/legate/agentic/loop.rb +134 -0
  65. data/lib/legate/agentic.rb +5 -0
  66. data/lib/legate/agents/loop_agent.rb +248 -0
  67. data/lib/legate/agents/parallel_agent.rb +163 -0
  68. data/lib/legate/agents/sequential_agent.rb +190 -0
  69. data/lib/legate/agents.rb +14 -0
  70. data/lib/legate/auth/config.rb +148 -0
  71. data/lib/legate/auth/coordinator.rb +218 -0
  72. data/lib/legate/auth/coordinators/oauth2_coordinator.rb +99 -0
  73. data/lib/legate/auth/coordinators/oidc_coordinator.rb +68 -0
  74. data/lib/legate/auth/coordinators/service_account_coordinator.rb +122 -0
  75. data/lib/legate/auth/credential.rb +157 -0
  76. data/lib/legate/auth/encryption.rb +108 -0
  77. data/lib/legate/auth/error.rb +94 -0
  78. data/lib/legate/auth/exchanged_credential.rb +180 -0
  79. data/lib/legate/auth/excon_middleware.rb +285 -0
  80. data/lib/legate/auth/http_client_utils.rb +364 -0
  81. data/lib/legate/auth/manager.rb +531 -0
  82. data/lib/legate/auth/manager_store.rb +394 -0
  83. data/lib/legate/auth/middleware_factory.rb +290 -0
  84. data/lib/legate/auth/runner.rb +279 -0
  85. data/lib/legate/auth/scheme.rb +125 -0
  86. data/lib/legate/auth/schemes/api_key.rb +212 -0
  87. data/lib/legate/auth/schemes/google_service_account.rb +108 -0
  88. data/lib/legate/auth/schemes/http_bearer.rb +98 -0
  89. data/lib/legate/auth/schemes/oauth2.rb +396 -0
  90. data/lib/legate/auth/schemes/openid_connect.rb +346 -0
  91. data/lib/legate/auth/schemes/service_account.rb +388 -0
  92. data/lib/legate/auth/schemes.rb +40 -0
  93. data/lib/legate/auth/token_manager.rb +362 -0
  94. data/lib/legate/auth/token_store.rb +86 -0
  95. data/lib/legate/auth/tool_context_extension.rb +97 -0
  96. data/lib/legate/auth/tool_integration.rb +188 -0
  97. data/lib/legate/auth/url_guard.rb +81 -0
  98. data/lib/legate/auth.rb +453 -0
  99. data/lib/legate/callbacks/callback_context.rb +71 -0
  100. data/lib/legate/cli/agent_commands.rb +950 -0
  101. data/lib/legate/cli/auth_commands.rb +520 -0
  102. data/lib/legate/cli/base_command.rb +24 -0
  103. data/lib/legate/cli/deployment_commands.rb +934 -0
  104. data/lib/legate/cli/output_helper.rb +108 -0
  105. data/lib/legate/cli/session_commands.rb +138 -0
  106. data/lib/legate/cli/skaffold_commands.rb +223 -0
  107. data/lib/legate/cli/tool_commands.rb +261 -0
  108. data/lib/legate/cli/web_commands.rb +182 -0
  109. data/lib/legate/cli.rb +40 -0
  110. data/lib/legate/configuration/webhooks.rb +113 -0
  111. data/lib/legate/configuration.rb +39 -0
  112. data/lib/legate/definition_store.rb +23 -0
  113. data/lib/legate/errors.rb +118 -0
  114. data/lib/legate/event.rb +161 -0
  115. data/lib/legate/gemini_ai_beta_patch.rb +39 -0
  116. data/lib/legate/generators/agent_generator.rb +412 -0
  117. data/lib/legate/generators/code_validator.rb +48 -0
  118. data/lib/legate/generators/legate/install_generator.rb +35 -0
  119. data/lib/legate/generators/legate/templates/create_legate_tables.rb.tt +36 -0
  120. data/lib/legate/generators/legate/templates/initializer.rb +18 -0
  121. data/lib/legate/generators/runtime_tool_loader.rb +76 -0
  122. data/lib/legate/generators/tool_generator.rb +408 -0
  123. data/lib/legate/generators.rb +11 -0
  124. data/lib/legate/global_definition_registry.rb +506 -0
  125. data/lib/legate/global_tool_manager.rb +135 -0
  126. data/lib/legate/llm/adapter.rb +69 -0
  127. data/lib/legate/llm/gemini.rb +172 -0
  128. data/lib/legate/llm/ollama.rb +80 -0
  129. data/lib/legate/llm.rb +34 -0
  130. data/lib/legate/mcp/client.rb +320 -0
  131. data/lib/legate/mcp/connection/sse.rb +292 -0
  132. data/lib/legate/mcp/connection/stdio.rb +273 -0
  133. data/lib/legate/mcp/connection_manager.rb +103 -0
  134. data/lib/legate/mcp/server/legate_agent_adapter.rb +170 -0
  135. data/lib/legate/mcp/server/legate_direct_agent_adapter.rb +140 -0
  136. data/lib/legate/mcp/server/legate_tool_adapter.rb +119 -0
  137. data/lib/legate/mcp/tool_wrapper.rb +138 -0
  138. data/lib/legate/mcp/util/schema_converter.rb +134 -0
  139. data/lib/legate/mcp.rb +23 -0
  140. data/lib/legate/plan_executor.rb +375 -0
  141. data/lib/legate/planner.rb +839 -0
  142. data/lib/legate/rails/railtie.rb +43 -0
  143. data/lib/legate/rails.rb +9 -0
  144. data/lib/legate/redaction.rb +32 -0
  145. data/lib/legate/session.rb +299 -0
  146. data/lib/legate/session_service/active_record.rb +300 -0
  147. data/lib/legate/session_service/base.rb +68 -0
  148. data/lib/legate/session_service/event_broadcast.rb +74 -0
  149. data/lib/legate/session_service/in_memory.rb +188 -0
  150. data/lib/legate/tool/metadata_dsl.rb +122 -0
  151. data/lib/legate/tool.rb +276 -0
  152. data/lib/legate/tool_code_generator.rb +103 -0
  153. data/lib/legate/tool_context.rb +350 -0
  154. data/lib/legate/tool_loader.rb +39 -0
  155. data/lib/legate/tool_registry.rb +73 -0
  156. data/lib/legate/tool_result.rb +61 -0
  157. data/lib/legate/tools/agent_tool.rb +187 -0
  158. data/lib/legate/tools/base/http_client.rb +319 -0
  159. data/lib/legate/tools/base/safe_url.rb +56 -0
  160. data/lib/legate/tools/base_async_job_tool.rb +91 -0
  161. data/lib/legate/tools/calculator.rb +89 -0
  162. data/lib/legate/tools/cat_facts.rb +81 -0
  163. data/lib/legate/tools/check_job_status_tool.rb +48 -0
  164. data/lib/legate/tools/current_time_tool.rb +64 -0
  165. data/lib/legate/tools/echo.rb +43 -0
  166. data/lib/legate/tools/http_request_tool.rb +105 -0
  167. data/lib/legate/tools/random_number_tool.rb +64 -0
  168. data/lib/legate/tools/read_webpage_tool.rb +92 -0
  169. data/lib/legate/tools/sleepy_tool.rb +74 -0
  170. data/lib/legate/tools/webhook_tool.rb +146 -0
  171. data/lib/legate/version.rb +5 -0
  172. data/lib/legate/web/app.rb +984 -0
  173. data/lib/legate/web/public/css/main.css +4980 -0
  174. data/lib/legate/web/public/images/favicon-256.png +0 -0
  175. data/lib/legate/web/public/images/favicon-32.png +0 -0
  176. data/lib/legate/web/public/images/legate-logo-dark.png +0 -0
  177. data/lib/legate/web/public/images/legate-logo-light.png +0 -0
  178. data/lib/legate/web/public/js/legate.js +616 -0
  179. data/lib/legate/web/public/styles/main.scss +4402 -0
  180. data/lib/legate/web/routes/agent_authentication_routes.rb +530 -0
  181. data/lib/legate/web/routes/agent_definition_routes.rb +803 -0
  182. data/lib/legate/web/routes/agent_generator_routes.rb +80 -0
  183. data/lib/legate/web/routes/agent_interaction_routes.rb +734 -0
  184. data/lib/legate/web/routes/agent_runtime_routes.rb +323 -0
  185. data/lib/legate/web/routes/api_routes.rb +56 -0
  186. data/lib/legate/web/routes/authentication_routes.rb +1541 -0
  187. data/lib/legate/web/routes/core_routes.rb +111 -0
  188. data/lib/legate/web/routes/documentation_routes.rb +220 -0
  189. data/lib/legate/web/routes/tool_generator_routes.rb +81 -0
  190. data/lib/legate/web/routes/tools_ui_routes.rb +207 -0
  191. data/lib/legate/web/sass_compiler.rb +73 -0
  192. data/lib/legate/web/views/_active_session_info.slim +25 -0
  193. data/lib/legate/web/views/_activity_list.slim +55 -0
  194. data/lib/legate/web/views/_agent_card.slim +56 -0
  195. data/lib/legate/web/views/_agent_generator_modal.slim +382 -0
  196. data/lib/legate/web/views/_agent_status_controls.slim +71 -0
  197. data/lib/legate/web/views/_agent_tool_table.slim +74 -0
  198. data/lib/legate/web/views/_chat_message.slim +95 -0
  199. data/lib/legate/web/views/_display_agent_configuration.slim +26 -0
  200. data/lib/legate/web/views/_display_agent_description.slim +11 -0
  201. data/lib/legate/web/views/_display_agent_fallback.slim +15 -0
  202. data/lib/legate/web/views/_display_agent_hierarchy.slim +93 -0
  203. data/lib/legate/web/views/_display_agent_instruction.slim +17 -0
  204. data/lib/legate/web/views/_display_agent_mcp.slim +13 -0
  205. data/lib/legate/web/views/_display_agent_model.slim +17 -0
  206. data/lib/legate/web/views/_display_agent_name.slim +42 -0
  207. data/lib/legate/web/views/_display_agent_output_key.slim +26 -0
  208. data/lib/legate/web/views/_display_agent_type.slim +65 -0
  209. data/lib/legate/web/views/_edit_agent_configuration.slim +74 -0
  210. data/lib/legate/web/views/_edit_agent_description.slim +16 -0
  211. data/lib/legate/web/views/_edit_agent_fallback.slim +25 -0
  212. data/lib/legate/web/views/_edit_agent_hierarchy.slim +98 -0
  213. data/lib/legate/web/views/_edit_agent_instruction.slim +49 -0
  214. data/lib/legate/web/views/_edit_agent_mcp.slim +33 -0
  215. data/lib/legate/web/views/_edit_agent_model.slim +23 -0
  216. data/lib/legate/web/views/_edit_agent_output_key.slim +36 -0
  217. data/lib/legate/web/views/_edit_agent_tools.slim +40 -0
  218. data/lib/legate/web/views/_edit_agent_type.slim +67 -0
  219. data/lib/legate/web/views/_session_error.slim +4 -0
  220. data/lib/legate/web/views/_skeleton.slim +69 -0
  221. data/lib/legate/web/views/_tool_card.slim +9 -0
  222. data/lib/legate/web/views/_tool_generator_modal.slim +311 -0
  223. data/lib/legate/web/views/agent.slim +436 -0
  224. data/lib/legate/web/views/agent_auth.slim +562 -0
  225. data/lib/legate/web/views/agents.slim +369 -0
  226. data/lib/legate/web/views/auth.slim +112 -0
  227. data/lib/legate/web/views/auth_credential_detail.slim +327 -0
  228. data/lib/legate/web/views/auth_credentials.slim +261 -0
  229. data/lib/legate/web/views/auth_debug.slim +94 -0
  230. data/lib/legate/web/views/auth_mapping_detail.slim +151 -0
  231. data/lib/legate/web/views/auth_mapping_new.slim +123 -0
  232. data/lib/legate/web/views/auth_mappings.slim +120 -0
  233. data/lib/legate/web/views/auth_scheme_detail.slim +274 -0
  234. data/lib/legate/web/views/auth_schemes.slim +259 -0
  235. data/lib/legate/web/views/auth_test.slim +418 -0
  236. data/lib/legate/web/views/chat.slim +192 -0
  237. data/lib/legate/web/views/docs_index.slim +105 -0
  238. data/lib/legate/web/views/docs_show.slim +105 -0
  239. data/lib/legate/web/views/error_404.slim +5 -0
  240. data/lib/legate/web/views/index.slim +148 -0
  241. data/lib/legate/web/views/layout.slim +144 -0
  242. data/lib/legate/web/views/tool_detail.slim +87 -0
  243. data/lib/legate/web/views/tools.slim +50 -0
  244. data/lib/legate/web/webhook_listener.rb +367 -0
  245. data/lib/legate/web.rb +9 -0
  246. data/lib/legate.rb +220 -0
  247. data/public/docs/advanced/callbacks.md +828 -0
  248. data/public/docs/advanced/mcp_schema_conversion.md +59 -0
  249. data/public/docs/authentication/api_reference/config.md +210 -0
  250. data/public/docs/authentication/api_reference/credential.md +246 -0
  251. data/public/docs/authentication/api_reference/encryption.md +218 -0
  252. data/public/docs/authentication/api_reference/exchanged_credential.md +271 -0
  253. data/public/docs/authentication/api_reference/excon_middleware.md +175 -0
  254. data/public/docs/authentication/api_reference/index.md +30 -0
  255. data/public/docs/authentication/api_reference/scheme.md +250 -0
  256. data/public/docs/authentication/api_reference/schemes/api_key.md +175 -0
  257. data/public/docs/authentication/api_reference/schemes/google_service_account.md +221 -0
  258. data/public/docs/authentication/api_reference/schemes/http_bearer.md +169 -0
  259. data/public/docs/authentication/api_reference/schemes/oauth2.md +343 -0
  260. data/public/docs/authentication/api_reference/schemes/oidc.md +73 -0
  261. data/public/docs/authentication/api_reference/schemes/openid_connect.md +311 -0
  262. data/public/docs/authentication/api_reference/schemes/service_account.md +287 -0
  263. data/public/docs/authentication/api_reference/token_manager.md +221 -0
  264. data/public/docs/authentication/api_reference/token_store.md +146 -0
  265. data/public/docs/authentication/api_reference/tool_context_extension.md +166 -0
  266. data/public/docs/authentication/guides/api_key.md +190 -0
  267. data/public/docs/authentication/guides/bearer.md +172 -0
  268. data/public/docs/authentication/guides/configuration.md +255 -0
  269. data/public/docs/authentication/guides/custom_flow.md +523 -0
  270. data/public/docs/authentication/guides/index.md +24 -0
  271. data/public/docs/authentication/guides/migration.md +435 -0
  272. data/public/docs/authentication/guides/oauth2.md +252 -0
  273. data/public/docs/authentication/guides/oidc.md +241 -0
  274. data/public/docs/authentication/guides/overview.md +155 -0
  275. data/public/docs/authentication/guides/secure_storage.md +301 -0
  276. data/public/docs/authentication/guides/service_account.md +228 -0
  277. data/public/docs/authentication/guides/token_lifecycle.md +295 -0
  278. data/public/docs/authentication/guides/web_ui_integration.md +504 -0
  279. data/public/docs/authentication/index.md +58 -0
  280. data/public/docs/authentication/troubleshooting/credential_storage.md +550 -0
  281. data/public/docs/authentication/troubleshooting/environment_variables.md +540 -0
  282. data/public/docs/authentication/troubleshooting/index.md +11 -0
  283. data/public/docs/authentication/troubleshooting/oauth2_issues.md +220 -0
  284. data/public/docs/authentication/troubleshooting/oidc_issues.md +412 -0
  285. data/public/docs/authentication/troubleshooting/token_refresh.md +338 -0
  286. data/public/docs/cli/legate_cli_usage.md +363 -0
  287. data/public/docs/core_concepts/legate_agent_lifecycle.md +124 -0
  288. data/public/docs/core_concepts/legate_architecture_overview.md +110 -0
  289. data/public/docs/core_concepts/legate_configuration.md +116 -0
  290. data/public/docs/core_concepts/legate_definition_store.md +102 -0
  291. data/public/docs/core_concepts/legate_planner.md +94 -0
  292. data/public/docs/core_concepts/legate_session_service.md +104 -0
  293. data/public/docs/error_handling/legate_error_handling.md +122 -0
  294. data/public/docs/examples.md +199 -0
  295. data/public/docs/getting_started.md +111 -0
  296. data/public/docs/guides/agentic_agents.md +137 -0
  297. data/public/docs/guides/ai_code_generators.md +437 -0
  298. data/public/docs/guides/auto_loading.md +326 -0
  299. data/public/docs/guides/configuring_agent_webhooks.md +219 -0
  300. data/public/docs/guides/http_client_usage.md +264 -0
  301. data/public/docs/guides/llm_providers.md +137 -0
  302. data/public/docs/guides/mcp_client_integration.md +232 -0
  303. data/public/docs/guides/mcp_server_exposure.md +206 -0
  304. data/public/docs/guides/rails_integration.md +128 -0
  305. data/public/docs/guides/sending_outbound_webhooks.md +227 -0
  306. data/public/docs/guides/streaming.md +112 -0
  307. data/public/docs/guides/webhooks.md +288 -0
  308. data/public/docs/introduction.md +51 -0
  309. data/public/docs/multi_agent_systems/advanced_features.md +57 -0
  310. data/public/docs/multi_agent_systems/agent_delegation.md +190 -0
  311. data/public/docs/multi_agent_systems/agent_hierarchy.md +49 -0
  312. data/public/docs/multi_agent_systems/state_management.md +47 -0
  313. data/public/docs/multi_agent_systems/workflow_agents.md +72 -0
  314. data/public/docs/tools/legate_built_in_tools.md +332 -0
  315. data/public/docs/tools/legate_tools_and_registry.md +263 -0
  316. data/public/docs/web_ui/legate_web_ui.md +137 -0
  317. metadata +823 -0
@@ -0,0 +1,1541 @@
1
+ # File: lib/legate/web/routes/authentication_routes.rb
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../../auth/url_guard' # SSRF guard for credential-test URLs
5
+
6
+ module Legate
7
+ module Web
8
+ module AuthenticationRoutes
9
+ def self.registered(app)
10
+ # Add helper methods to the app
11
+ app.helpers do
12
+ # Helper method to get a description for a scheme
13
+ def get_scheme_description(scheme)
14
+ case scheme.scheme_type
15
+ when :api_key
16
+ 'Simple API key authentication for services that use API keys in headers or query parameters'
17
+ when :http_bearer
18
+ 'HTTP Bearer token authentication and basic auth support'
19
+ when :oauth2
20
+ 'OAuth2 authorization code flow for secure third-party authentication'
21
+ when :oidc, :openid_connect
22
+ 'OpenID Connect authentication extending OAuth2 with identity information'
23
+ when :service_account
24
+ 'Service account authentication with automatic token exchange'
25
+ when :google_service_account
26
+ 'Google Cloud service account authentication with JSON key files'
27
+ else
28
+ "Authentication scheme of type: #{scheme.scheme_type}"
29
+ end
30
+ end
31
+
32
+ # Helper method to get a description for a credential
33
+ def get_credential_description(credential)
34
+ case credential.auth_type
35
+ when :api_key
36
+ 'API key credential for service authentication'
37
+ when :oauth2, :oidc
38
+ 'OAuth2/OIDC client credentials for authorization flows'
39
+ when :service_account, :google_service_account
40
+ 'Service account credentials for automated authentication'
41
+ when :http_bearer
42
+ 'Bearer token or basic auth credentials'
43
+ else
44
+ "Credential of type: #{credential.auth_type}"
45
+ end
46
+ end
47
+
48
+ # Helper method to get masked credential information for display
49
+ def get_masked_credential_info(credential)
50
+ info_parts = []
51
+
52
+ # Check for common credential fields and mask them appropriately
53
+ if credential[:api_key, resolve_env: false]
54
+ masked_key = mask_sensitive_value(credential[:api_key, resolve_env: false])
55
+ info_parts << "API Key: #{masked_key}"
56
+ end
57
+
58
+ info_parts << "Client ID: #{credential[:client_id, resolve_env: false]}" if credential[:client_id, resolve_env: false]
59
+
60
+ if credential[:client_secret, resolve_env: false]
61
+ masked_secret = mask_sensitive_value(credential[:client_secret, resolve_env: false])
62
+ info_parts << "Client Secret: #{masked_secret}"
63
+ end
64
+
65
+ if credential[:bearer_token, resolve_env: false]
66
+ masked_token = mask_sensitive_value(credential[:bearer_token, resolve_env: false])
67
+ info_parts << "Bearer Token: #{masked_token}"
68
+ end
69
+
70
+ info_parts << "Username: #{credential[:username, resolve_env: false]}" if credential[:username, resolve_env: false]
71
+
72
+ info_parts << "Password: #{mask_sensitive_value(credential[:password, resolve_env: false])}" if credential[:password, resolve_env: false]
73
+
74
+ info_parts.empty? ? 'No displayable information' : info_parts.join(', ')
75
+ end
76
+
77
+ # Helper method to mask sensitive values for display
78
+ def mask_sensitive_value(value)
79
+ return '[Not Set]' if value.nil? || value.empty?
80
+
81
+ value_str = value.to_s
82
+ # Never reveal the head, and never reveal any characters of a short
83
+ # secret (the old first-3/last-3 mask exposed "secret" as "sec***ret").
84
+ return '••••••••' if value_str.length < 12
85
+
86
+ "••••••••#{value_str[-4..]}"
87
+ end
88
+
89
+ # Helper method to get scheme configuration fields
90
+ def get_scheme_config_fields(scheme_type)
91
+ case scheme_type.to_sym
92
+ when :api_key
93
+ [] # API Key scheme has no configuration
94
+ when :http_bearer
95
+ [] # HTTP Bearer scheme has no configuration
96
+ when :oauth2
97
+ [
98
+ { name: 'authorization_url', type: 'url', required: true, label: 'Authorization URL' },
99
+ { name: 'token_url', type: 'url', required: true, label: 'Token URL' },
100
+ { name: 'scopes', type: 'text', required: false, label: 'Scopes (space-separated)' },
101
+ { name: 'use_pkce', type: 'checkbox', required: false, label: 'Use PKCE' },
102
+ { name: 'revocation_url', type: 'url', required: false, label: 'Revocation URL' }
103
+ ]
104
+ when :oidc, :openid_connect
105
+ [
106
+ { name: 'authorization_url', type: 'url', required: true, label: 'Authorization URL' },
107
+ { name: 'token_url', type: 'url', required: true, label: 'Token URL' },
108
+ { name: 'userinfo_url', type: 'url', required: false, label: 'UserInfo URL' },
109
+ { name: 'scopes', type: 'text', required: false, label: 'Scopes (space-separated)' },
110
+ { name: 'use_pkce', type: 'checkbox', required: false, label: 'Use PKCE' }
111
+ ]
112
+ when :service_account
113
+ [
114
+ { name: 'token_url', type: 'url', required: true, label: 'Token URL' },
115
+ { name: 'scopes', type: 'text', required: false, label: 'Scopes (space-separated)' }
116
+ ]
117
+ when :google_service_account
118
+ [
119
+ { name: 'scopes', type: 'text', required: false, label: 'Scopes (space-separated)' }
120
+ ]
121
+ else
122
+ []
123
+ end
124
+ end
125
+
126
+ # Helper method to get current scheme configuration
127
+ def get_scheme_current_config(scheme)
128
+ config = {}
129
+ case scheme.scheme_type
130
+ when :oauth2, :oidc, :openid_connect
131
+ config['authorization_url'] = scheme.authorization_url if scheme.respond_to?(:authorization_url)
132
+ config['token_url'] = scheme.token_url if scheme.respond_to?(:token_url)
133
+ config['scopes'] = scheme.scopes.join(' ') if scheme.respond_to?(:scopes) && scheme.scopes
134
+ config['use_pkce'] = scheme.use_pkce if scheme.respond_to?(:use_pkce)
135
+ config['revocation_url'] = scheme.revocation_url if scheme.respond_to?(:revocation_url)
136
+ config['userinfo_url'] = scheme.userinfo_url if scheme.respond_to?(:userinfo_url)
137
+ when :service_account, :google_service_account
138
+ config['token_url'] = scheme.token_url if scheme.respond_to?(:token_url)
139
+ config['scopes'] = scheme.scopes.join(' ') if scheme.respond_to?(:scopes) && scheme.scopes
140
+ end
141
+ config
142
+ end
143
+
144
+ # Helper method to get compatible credential types for a scheme
145
+ def get_compatible_credential_types(scheme_type)
146
+ case scheme_type.to_sym
147
+ when :api_key
148
+ ['api_key']
149
+ when :http_bearer
150
+ %w[http_bearer bearer_token]
151
+ when :oauth2, :oidc, :openid_connect
152
+ %w[oauth2 oidc]
153
+ when :service_account, :google_service_account
154
+ %w[service_account google_service_account]
155
+ else
156
+ []
157
+ end
158
+ end
159
+
160
+ # Helper method to get credential configuration fields
161
+ def get_credential_config_fields(auth_type)
162
+ case auth_type.to_sym
163
+ when :api_key
164
+ [
165
+ { name: 'api_key', type: 'password', required: true, label: 'API Key', placeholder: 'Enter your API key' },
166
+ { name: 'location', type: 'select', required: false, label: 'Location', options: %w[header query cookie], default: 'header' },
167
+ { name: 'name', type: 'text', required: false, label: 'Parameter Name', placeholder: 'X-API-Key' }
168
+ ]
169
+ when :http_bearer
170
+ [
171
+ { name: 'bearer_token', type: 'password', required: true, label: 'Bearer Token', placeholder: 'Enter bearer token' }
172
+ ]
173
+ when :oauth2, :oidc
174
+ [
175
+ { name: 'client_id', type: 'text', required: true, label: 'Client ID', placeholder: 'Enter client ID' },
176
+ { name: 'client_secret', type: 'password', required: true, label: 'Client Secret', placeholder: 'Enter client secret' },
177
+ { name: 'redirect_uri', type: 'url', required: false, label: 'Redirect URI', placeholder: 'https://your-app.com/callback' },
178
+ { name: 'scopes', type: 'text', required: false, label: 'Scopes (space-separated)', placeholder: 'read write' }
179
+ ]
180
+ when :service_account
181
+ [
182
+ { name: 'client_email', type: 'email', required: true, label: 'Client Email', placeholder: 'service-account@project.iam.gserviceaccount.com' },
183
+ { name: 'private_key', type: 'textarea', required: true, label: 'Private Key', placeholder: '-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----' },
184
+ { name: 'project_id', type: 'text', required: false, label: 'Project ID', placeholder: 'your-project-id' }
185
+ ]
186
+ when :google_service_account
187
+ [
188
+ { name: 'service_account_key', type: 'textarea', required: true, label: 'Service Account Key (JSON)', placeholder: '{"type": "service_account", "project_id": "..."}' }
189
+ ]
190
+ when :basic
191
+ [
192
+ { name: 'username', type: 'text', required: true, label: 'Username', placeholder: 'Enter username' },
193
+ { name: 'password', type: 'password', required: true, label: 'Password', placeholder: 'Enter password' }
194
+ ]
195
+ else
196
+ []
197
+ end
198
+ end
199
+
200
+ # Helper method to get current credential configuration (masked)
201
+ def get_credential_current_config(credential)
202
+ config = {}
203
+
204
+ # Get all attributes but mask sensitive ones
205
+ credential.to_h(resolve_env: false).each do |key, value|
206
+ next if key == :auth_type
207
+
208
+ config[key.to_s] = if sensitive_credential_field?(key)
209
+ mask_sensitive_value(value)
210
+ else
211
+ value
212
+ end
213
+ end
214
+
215
+ config
216
+ end
217
+
218
+ # Helper method to check if a credential field is sensitive
219
+ def sensitive_credential_field?(field_name)
220
+ sensitive_fields = %i[api_key client_secret bearer_token password private_key service_account_key token]
221
+ sensitive_fields.include?(field_name.to_sym)
222
+ end
223
+
224
+ # Helper method to get compatible schemes for a credential type
225
+ def get_compatible_schemes_for_credential(auth_type)
226
+ auth_manager = Legate::Auth::Manager.instance
227
+ schemes = auth_manager.schemes || {}
228
+
229
+ compatible_schemes = schemes.select do |scheme_name, scheme|
230
+ case auth_type.to_sym
231
+ when :api_key
232
+ scheme.scheme_type == :api_key
233
+ when :http_bearer
234
+ scheme.scheme_type == :http_bearer
235
+ when :oauth2, :oidc
236
+ %i[oauth2 oidc openid_connect].include?(scheme.scheme_type)
237
+ when :service_account, :google_service_account
238
+ %i[service_account google_service_account].include?(scheme.scheme_type)
239
+ when :basic
240
+ scheme.scheme_type == :http_bearer
241
+ else
242
+ false
243
+ end
244
+ end
245
+
246
+ compatible_schemes.keys
247
+ end
248
+
249
+ # Helper method to get mapping configuration fields
250
+ def get_mapping_config_fields
251
+ [
252
+ { name: 'pattern', type: 'text', required: true, label: 'URL Pattern', placeholder: 'e.g. https://api.example.com/v1/* or ^https://.*\\.example\\.com/.*$' },
253
+ { name: 'pattern_type', type: 'select', required: true, label: 'Pattern Type', options: %w[string regex], default: 'string' },
254
+ { name: 'scheme_name', type: 'select', required: true, label: 'Authentication Scheme' },
255
+ { name: 'credential_name', type: 'select', required: true, label: 'Credential' },
256
+ { name: 'priority', type: 'number', required: false, label: 'Priority', placeholder: '1' },
257
+ { name: 'active', type: 'checkbox', required: false, label: 'Active', default: true }
258
+ ]
259
+ end
260
+
261
+ # Helper to validate a mapping pattern
262
+ def valid_mapping_pattern?(pattern, pattern_type)
263
+ return false if pattern.nil? || pattern.strip.empty?
264
+
265
+ if pattern_type == 'regex'
266
+ begin
267
+ Regexp.new(pattern)
268
+ true
269
+ rescue RegexpError
270
+ false
271
+ end
272
+ else
273
+ true
274
+ end
275
+ end
276
+
277
+ # Helper to check scheme/credential compatibility
278
+ def mapping_scheme_credential_compatible?(scheme, credential)
279
+ return false unless scheme && credential
280
+
281
+ compatible_types = get_compatible_credential_types(scheme.scheme_type)
282
+ compatible_types.include?(credential.auth_type.to_s) || compatible_types.include?(credential.auth_type)
283
+ end
284
+
285
+ # Helper method to perform credential testing
286
+ def test_credential_functionality(credential, test_options = {})
287
+ test_results = { success: true, tests: [], credential_name: credential.to_s }
288
+
289
+ begin
290
+ # Test 1: Basic validation
291
+ credential.to_h(resolve_env: true)
292
+ test_results[:tests] << {
293
+ name: 'Basic Validation',
294
+ status: 'passed',
295
+ message: 'Credential structure and environment variables are valid'
296
+ }
297
+ rescue StandardError => e
298
+ test_results[:success] = false
299
+ test_results[:tests] << {
300
+ name: 'Basic Validation',
301
+ status: 'failed',
302
+ message: "Validation failed: #{e.message}"
303
+ }
304
+ return test_results
305
+ end
306
+
307
+ # Test 2: Type-specific validation
308
+ test_results[:tests] << case credential.auth_type
309
+ when :api_key
310
+ test_api_key_credential(credential, test_options)
311
+ when :oauth2, :oidc
312
+ test_oauth_credential(credential, test_options)
313
+ when :service_account, :google_service_account
314
+ test_service_account_credential(credential, test_options)
315
+ when :http_bearer
316
+ test_bearer_credential(credential, test_options)
317
+ else
318
+ {
319
+ name: 'Type-Specific Test',
320
+ status: 'skipped',
321
+ message: "No specific test available for credential type: #{credential.auth_type}"
322
+ }
323
+ end
324
+
325
+ # Check if any test failed
326
+ test_results[:success] = test_results[:tests].all? { |test| test[:status] != 'failed' }
327
+ test_results
328
+ end
329
+
330
+ # Test API key credential
331
+ def test_api_key_credential(credential, options = {})
332
+ api_key = credential[:api_key]
333
+ return { name: 'API Key Test', status: 'failed', message: 'API key is missing' } unless api_key
334
+
335
+ if options[:test_url]
336
+ # Test against provided URL
337
+ begin
338
+ require 'net/http'
339
+ Legate::Auth::UrlGuard.validate!(options[:test_url], label: 'Test URL')
340
+ uri = URI(options[:test_url])
341
+ http = Net::HTTP.new(uri.host, uri.port)
342
+ http.use_ssl = uri.scheme == 'https'
343
+ http.read_timeout = 10
344
+
345
+ request = Net::HTTP::Get.new(uri)
346
+
347
+ # Add API key based on location
348
+ location = credential[:location] || 'header'
349
+ key_name = credential[:name] || 'X-API-Key'
350
+
351
+ case location
352
+ when 'header'
353
+ request[key_name] = api_key
354
+ when 'query'
355
+ uri.query = "#{key_name}=#{api_key}"
356
+ request = Net::HTTP::Get.new(uri)
357
+ end
358
+
359
+ response = http.request(request)
360
+
361
+ if response.code.to_i < 400
362
+ { name: 'API Key Test', status: 'passed', message: "API key accepted (HTTP #{response.code})" }
363
+ else
364
+ { name: 'API Key Test', status: 'failed', message: "API key rejected (HTTP #{response.code})" }
365
+ end
366
+ rescue StandardError => e
367
+ { name: 'API Key Test', status: 'failed', message: "Network error: #{e.message}" }
368
+ end
369
+ else
370
+ { name: 'API Key Test', status: 'passed', message: 'API key format is valid (no test URL provided)' }
371
+ end
372
+ end
373
+
374
+ # Test OAuth credential
375
+ def test_oauth_credential(credential, _options = {})
376
+ client_id = credential[:client_id]
377
+ client_secret = credential[:client_secret]
378
+
379
+ return { name: 'OAuth Test', status: 'failed', message: 'Client ID is missing' } unless client_id
380
+ return { name: 'OAuth Test', status: 'failed', message: 'Client secret is missing' } unless client_secret
381
+
382
+ { name: 'OAuth Test', status: 'passed', message: 'OAuth credentials are properly formatted' }
383
+ end
384
+
385
+ # Test service account credential
386
+ def test_service_account_credential(credential, _options = {})
387
+ if credential.auth_type == :google_service_account
388
+ key_data = credential[:service_account_key]
389
+ return { name: 'Service Account Test', status: 'failed', message: 'Service account key is missing' } unless key_data
390
+
391
+ begin
392
+ parsed_key = JSON.parse(key_data)
393
+ required_fields = %w[type project_id private_key_id private_key client_email client_id]
394
+ missing_fields = required_fields.reject { |field| parsed_key.key?(field) }
395
+
396
+ if missing_fields.empty?
397
+ { name: 'Service Account Test', status: 'passed', message: 'Service account key is valid JSON with all required fields' }
398
+ else
399
+ { name: 'Service Account Test', status: 'failed', message: "Missing required fields: #{missing_fields.join(', ')}" }
400
+ end
401
+ rescue JSON::ParserError
402
+ { name: 'Service Account Test', status: 'failed', message: 'Service account key is not valid JSON' }
403
+ end
404
+ else
405
+ client_email = credential[:client_email]
406
+ private_key = credential[:private_key]
407
+
408
+ return { name: 'Service Account Test', status: 'failed', message: 'Client email is missing' } unless client_email
409
+ return { name: 'Service Account Test', status: 'failed', message: 'Private key is missing' } unless private_key
410
+
411
+ if private_key.include?('BEGIN PRIVATE KEY')
412
+ { name: 'Service Account Test', status: 'passed', message: 'Service account credentials are properly formatted' }
413
+ else
414
+ { name: 'Service Account Test', status: 'failed', message: 'Private key does not appear to be in PEM format' }
415
+ end
416
+ end
417
+ end
418
+
419
+ # Test bearer token credential
420
+ def test_bearer_credential(credential, options = {})
421
+ bearer_token = credential[:bearer_token]
422
+ return { name: 'Bearer Token Test', status: 'failed', message: 'Bearer token is missing' } unless bearer_token
423
+
424
+ if options[:test_url]
425
+ begin
426
+ require 'net/http'
427
+ Legate::Auth::UrlGuard.validate!(options[:test_url], label: 'Test URL')
428
+ uri = URI(options[:test_url])
429
+ http = Net::HTTP.new(uri.host, uri.port)
430
+ http.use_ssl = uri.scheme == 'https'
431
+ http.read_timeout = 10
432
+
433
+ request = Net::HTTP::Get.new(uri)
434
+ request['Authorization'] = "Bearer #{bearer_token}"
435
+
436
+ response = http.request(request)
437
+
438
+ if response.code.to_i < 400
439
+ { name: 'Bearer Token Test', status: 'passed', message: "Bearer token accepted (HTTP #{response.code})" }
440
+ else
441
+ { name: 'Bearer Token Test', status: 'failed', message: "Bearer token rejected (HTTP #{response.code})" }
442
+ end
443
+ rescue StandardError => e
444
+ { name: 'Bearer Token Test', status: 'failed', message: "Network error: #{e.message}" }
445
+ end
446
+ else
447
+ { name: 'Bearer Token Test', status: 'passed', message: 'Bearer token format is valid (no test URL provided)' }
448
+ end
449
+ end
450
+
451
+ # Helper to test API calls with authentication
452
+ def test_authenticated_api_call(url, scheme_name, credential_name, _options = {})
453
+ auth_manager = Legate::Auth::Manager.instance
454
+ scheme = auth_manager.get_scheme(scheme_name.to_sym)
455
+ credential = auth_manager.get_credential(credential_name.to_sym)
456
+
457
+ return { success: false, error: 'Scheme not found' } unless scheme
458
+ return { success: false, error: 'Credential not found' } unless credential
459
+ return { success: false, error: 'Scheme and credential are not compatible' } unless mapping_scheme_credential_compatible?(scheme, credential)
460
+
461
+ begin
462
+ require 'net/http'
463
+ Legate::Auth::UrlGuard.validate!(url, label: 'Test URL')
464
+ uri = URI(url)
465
+ http = Net::HTTP.new(uri.host, uri.port)
466
+ http.use_ssl = uri.scheme == 'https'
467
+ http.read_timeout = 15
468
+
469
+ request = Net::HTTP::Get.new(uri)
470
+
471
+ # Apply authentication based on scheme type
472
+ case scheme.scheme_type
473
+ when :api_key
474
+ location = credential[:location] || 'header'
475
+ key_name = credential[:name] || 'X-API-Key'
476
+ api_key = credential[:api_key]
477
+
478
+ case location
479
+ when 'header'
480
+ request[key_name] = api_key
481
+ when 'query'
482
+ uri.query = "#{key_name}=#{api_key}"
483
+ request = Net::HTTP::Get.new(uri)
484
+ end
485
+ when :http_bearer
486
+ request['Authorization'] = "Bearer #{credential[:bearer_token]}"
487
+ when :oauth2, :oidc
488
+ # For OAuth2, we'd need an access token, which requires a full flow
489
+ return { success: false, error: 'OAuth2 testing requires a complete authentication flow' }
490
+ when :service_account, :google_service_account
491
+ # Service account testing would require token generation
492
+ return { success: false, error: 'Service account testing requires token generation' }
493
+ end
494
+
495
+ response = http.request(request)
496
+
497
+ {
498
+ success: true,
499
+ status_code: response.code.to_i,
500
+ status_message: response.message,
501
+ headers: response.to_hash,
502
+ body_preview: response.body&.slice(0, 500),
503
+ authenticated: response.code.to_i < 400
504
+ }
505
+ rescue StandardError => e
506
+ { success: false, error: "Network error: #{e.message}" }
507
+ end
508
+ end
509
+ end
510
+
511
+ # GET /auth - Main authentication management dashboard
512
+ app.get '/auth' do
513
+ logger.info('GET /auth route handler entered (from AuthenticationRoutes)')
514
+
515
+ # Access the authentication manager
516
+ auth_manager = Legate::Auth::Manager.instance
517
+
518
+ # Get basic counts for dashboard
519
+ schemes_count = auth_manager.schemes&.size || 0
520
+ credentials_count = auth_manager.credentials&.size || 0
521
+ mappings_count = auth_manager.url_mappings&.size || 0
522
+
523
+ # Set instance variables for the view
524
+ instance_variable_set(:@auth_manager_available, true)
525
+ instance_variable_set(:@schemes_count, schemes_count)
526
+ instance_variable_set(:@credentials_count, credentials_count)
527
+ instance_variable_set(:@mappings_count, mappings_count)
528
+
529
+ slim :auth
530
+ rescue StandardError => e
531
+ logger.error("Error in /auth route (from AuthenticationRoutes): #{e.class} - #{e.message}")
532
+ instance_variable_set(:@auth_manager_available, false)
533
+ instance_variable_set(:@error_message, e.message)
534
+ slim :auth
535
+ end
536
+
537
+ # GET /auth/schemes - List all available authentication schemes
538
+ app.get '/auth/schemes' do
539
+ logger.info('GET /auth/schemes route handler entered (from AuthenticationRoutes)')
540
+ content_type :html
541
+
542
+ auth_manager = Legate::Auth::Manager.instance
543
+ schemes = auth_manager.schemes || {}
544
+ credentials = auth_manager.credentials || {}
545
+ url_mappings = auth_manager.url_mappings || []
546
+
547
+ # Convert schemes to a more view-friendly format with usage information
548
+ schemes_data = schemes.map do |name, scheme|
549
+ # Find compatible credentials
550
+ compatible_credentials = credentials.select do |cred_name, credential|
551
+ auth_manager.send(:credential_compatible_with_scheme?, credential, scheme)
552
+ end
553
+
554
+ # Find URL mappings using this scheme
555
+ scheme_mappings = url_mappings.select { |mapping| mapping[:scheme_name] == name }
556
+
557
+ {
558
+ name: name,
559
+ scheme_type: scheme.scheme_type,
560
+ class_name: scheme.class.name.split('::').last,
561
+ description: get_scheme_description(scheme),
562
+ compatible_credentials_count: compatible_credentials.size,
563
+ url_mappings_count: scheme_mappings.size,
564
+ config_fields: get_scheme_config_fields(scheme.scheme_type),
565
+ has_config: !get_scheme_config_fields(scheme.scheme_type).empty?
566
+ }
567
+ end
568
+
569
+ instance_variable_set(:@schemes, schemes_data)
570
+ slim :auth_schemes
571
+ rescue StandardError => e
572
+ logger.error("Error in /auth/schemes route (from AuthenticationRoutes): #{e.class} - #{e.message}")
573
+ halt 500, "Error loading authentication schemes: #{e.message}"
574
+ end
575
+
576
+ # GET /auth/schemes/:name - Individual scheme details and configuration
577
+ app.get '/auth/schemes/:name' do
578
+ logger.info("GET /auth/schemes/#{params[:name]} route handler entered (from AuthenticationRoutes)")
579
+ content_type :html
580
+
581
+ scheme_name = params[:name].to_sym
582
+ auth_manager = Legate::Auth::Manager.instance
583
+ scheme = auth_manager.get_scheme(scheme_name)
584
+
585
+ halt 404, "Scheme not found: #{Rack::Utils.escape_html(params[:name])}" unless scheme
586
+
587
+ credentials = auth_manager.credentials || {}
588
+ url_mappings = auth_manager.url_mappings || []
589
+
590
+ # Find compatible credentials
591
+ compatible_credentials = credentials.select do |cred_name, credential|
592
+ auth_manager.send(:credential_compatible_with_scheme?, credential, scheme)
593
+ end.map do |cred_name, credential|
594
+ {
595
+ name: cred_name,
596
+ auth_type: credential.auth_type,
597
+ description: get_credential_description(credential),
598
+ masked_info: get_masked_credential_info(credential)
599
+ }
600
+ end
601
+
602
+ # Find URL mappings using this scheme
603
+ scheme_mappings = url_mappings.select { |mapping| mapping[:scheme_name] == scheme_name }
604
+ .map.with_index do |mapping, index|
605
+ {
606
+ id: index,
607
+ pattern: mapping[:pattern].is_a?(Regexp) ? mapping[:pattern].source : mapping[:pattern].to_s,
608
+ pattern_type: mapping[:pattern].is_a?(Regexp) ? 'regex' : 'string',
609
+ credential_name: mapping[:credential_name]
610
+ }
611
+ end
612
+
613
+ scheme_data = {
614
+ name: scheme_name,
615
+ scheme_type: scheme.scheme_type,
616
+ class_name: scheme.class.name.split('::').last,
617
+ description: get_scheme_description(scheme),
618
+ config_fields: get_scheme_config_fields(scheme.scheme_type),
619
+ current_config: get_scheme_current_config(scheme),
620
+ compatible_credential_types: get_compatible_credential_types(scheme.scheme_type),
621
+ compatible_credentials: compatible_credentials,
622
+ url_mappings: scheme_mappings
623
+ }
624
+
625
+ instance_variable_set(:@scheme, scheme_data)
626
+ slim :auth_scheme_detail
627
+ rescue StandardError => e
628
+ logger.error("Error in /auth/schemes/#{params[:name]} route (from AuthenticationRoutes): #{e.class} - #{e.message}")
629
+ halt 500, "Error loading scheme details: #{e.message}"
630
+ end
631
+
632
+ # POST /auth/schemes - Register new scheme instance
633
+ app.post '/auth/schemes' do
634
+ logger.info('POST /auth/schemes route handler entered (from AuthenticationRoutes)')
635
+ content_type :json
636
+
637
+ scheme_type = params[:scheme_type]&.to_sym
638
+ scheme_name = params[:scheme_name]&.to_sym
639
+
640
+ halt 400, { error: 'Scheme type is required' }.to_json unless scheme_type
641
+ halt 400, { error: 'Scheme name is required' }.to_json unless scheme_name
642
+
643
+ auth_manager = Legate::Auth::Manager.instance
644
+
645
+ # Check if scheme name already exists
646
+ halt 400, { error: "Scheme with name '#{scheme_name}' already exists" }.to_json if auth_manager.get_scheme(scheme_name)
647
+
648
+ begin
649
+ # Create new scheme instance based on type
650
+ scheme = case scheme_type
651
+ when :api_key
652
+ Legate::Auth::Schemes::ApiKey.new
653
+ when :http_bearer
654
+ Legate::Auth::Schemes::HTTPBearer.new
655
+ when :oauth2
656
+ Legate::Auth::Schemes::OAuth2.new(
657
+ authorization_url: params[:authorization_url],
658
+ token_url: params[:token_url],
659
+ scopes: params[:scopes]&.split(/\s+/),
660
+ use_pkce: params[:use_pkce] == 'true',
661
+ revocation_url: params[:revocation_url]
662
+ )
663
+ when :oidc, :openid_connect
664
+ Legate::Auth::Schemes::OpenIDConnect.new(
665
+ authorization_url: params[:authorization_url],
666
+ token_url: params[:token_url],
667
+ userinfo_url: params[:userinfo_url],
668
+ scopes: params[:scopes]&.split(/\s+/),
669
+ use_pkce: params[:use_pkce] == 'true'
670
+ )
671
+ when :service_account
672
+ Legate::Auth::Schemes::ServiceAccount.new(
673
+ token_url: params[:token_url],
674
+ scopes: params[:scopes]&.split(/\s+/)
675
+ )
676
+ when :google_service_account
677
+ Legate::Auth::Schemes::GoogleServiceAccount.new(
678
+ scopes: params[:scopes]&.split(/\s+/)
679
+ )
680
+ else
681
+ halt 400, { error: "Unsupported scheme type: #{scheme_type}" }.to_json
682
+ end
683
+
684
+ # Register the new scheme
685
+ auth_manager.register_scheme(scheme, scheme_name)
686
+
687
+ logger.info("Successfully registered new scheme '#{scheme_name}' of type '#{scheme_type}'")
688
+ { success: true, message: "Scheme '#{scheme_name}' registered successfully" }.to_json
689
+ rescue StandardError => e
690
+ logger.error("Error registering scheme '#{scheme_name}': #{e.class} - #{e.message}")
691
+ halt 500, { error: "Failed to register scheme: #{e.message}" }.to_json
692
+ end
693
+ end
694
+
695
+ # PUT /auth/schemes/:name - Update scheme configuration
696
+ app.put '/auth/schemes/:name' do
697
+ logger.info("PUT /auth/schemes/#{params[:name]} route handler entered (from AuthenticationRoutes)")
698
+ content_type :json
699
+
700
+ scheme_name = params[:name].to_sym
701
+ auth_manager = Legate::Auth::Manager.instance
702
+ existing_scheme = auth_manager.get_scheme(scheme_name)
703
+
704
+ halt 404, { error: "Scheme not found: #{params[:name]}" }.to_json unless existing_scheme
705
+
706
+ begin
707
+ # Create updated scheme instance with new configuration
708
+ scheme_type = existing_scheme.scheme_type
709
+ updated_scheme = case scheme_type
710
+ when :oauth2
711
+ Legate::Auth::Schemes::OAuth2.new(
712
+ authorization_url: params[:authorization_url],
713
+ token_url: params[:token_url],
714
+ scopes: params[:scopes]&.split(/\s+/),
715
+ use_pkce: params[:use_pkce] == 'true',
716
+ revocation_url: params[:revocation_url]
717
+ )
718
+ when :oidc, :openid_connect
719
+ Legate::Auth::Schemes::OpenIDConnect.new(
720
+ authorization_url: params[:authorization_url],
721
+ token_url: params[:token_url],
722
+ userinfo_url: params[:userinfo_url],
723
+ scopes: params[:scopes]&.split(/\s+/),
724
+ use_pkce: params[:use_pkce] == 'true'
725
+ )
726
+ when :service_account
727
+ Legate::Auth::Schemes::ServiceAccount.new(
728
+ token_url: params[:token_url],
729
+ scopes: params[:scopes]&.split(/\s+/)
730
+ )
731
+ when :google_service_account
732
+ Legate::Auth::Schemes::GoogleServiceAccount.new(
733
+ scopes: params[:scopes]&.split(/\s+/)
734
+ )
735
+ else
736
+ halt 400, { error: "Scheme type '#{scheme_type}' does not support configuration updates" }.to_json
737
+ end
738
+
739
+ # Replace the existing scheme
740
+ auth_manager.register_scheme(updated_scheme, scheme_name)
741
+
742
+ logger.info("Successfully updated scheme '#{scheme_name}' configuration")
743
+ { success: true, message: "Scheme '#{scheme_name}' updated successfully" }.to_json
744
+ rescue StandardError => e
745
+ logger.error("Error updating scheme '#{scheme_name}': #{e.class} - #{e.message}")
746
+ halt 500, { error: "Failed to update scheme: #{e.message}" }.to_json
747
+ end
748
+ end
749
+
750
+ # DELETE /auth/schemes/:name - Remove scheme instance
751
+ app.delete '/auth/schemes/:name' do
752
+ logger.info("DELETE /auth/schemes/#{params[:name]} route handler entered (from AuthenticationRoutes)")
753
+ content_type :json
754
+
755
+ scheme_name = params[:name].to_sym
756
+ auth_manager = Legate::Auth::Manager.instance
757
+ scheme = auth_manager.get_scheme(scheme_name)
758
+
759
+ halt 404, { error: "Scheme not found: #{params[:name]}" }.to_json unless scheme
760
+
761
+ # Check if scheme is used in URL mappings
762
+ url_mappings = auth_manager.url_mappings || []
763
+ dependent_mappings = url_mappings.select { |mapping| mapping[:scheme_name] == scheme_name }
764
+
765
+ if dependent_mappings.any?
766
+ mapping_patterns = dependent_mappings.map { |m| m[:pattern] }.join(', ')
767
+ halt 400, {
768
+ error: "Cannot delete scheme '#{scheme_name}' - it is used by URL mappings: #{mapping_patterns}"
769
+ }.to_json
770
+ end
771
+
772
+ begin
773
+ # Remove the scheme from the manager (persists via the store)
774
+ auth_manager.unregister_scheme(scheme_name)
775
+
776
+ logger.info("Successfully deleted scheme '#{scheme_name}'")
777
+ { success: true, message: "Scheme '#{scheme_name}' deleted successfully" }.to_json
778
+ rescue StandardError => e
779
+ logger.error("Error deleting scheme '#{scheme_name}': #{e.class} - #{e.message}")
780
+ halt 500, { error: "Failed to delete scheme: #{e.message}" }.to_json
781
+ end
782
+ end
783
+
784
+ # GET /auth/credentials - List all configured credentials
785
+ app.get '/auth/credentials' do
786
+ logger.info('GET /auth/credentials route handler entered (from AuthenticationRoutes)')
787
+ content_type :html
788
+
789
+ auth_manager = Legate::Auth::Manager.instance
790
+ credentials = auth_manager.credentials || {}
791
+ url_mappings = auth_manager.url_mappings || []
792
+
793
+ # Convert credentials to a view-friendly format with usage information
794
+ credentials_data = credentials.map do |name, credential|
795
+ # Find URL mappings using this credential
796
+ credential_mappings = url_mappings.select { |mapping| mapping[:credential_name] == name }
797
+
798
+ # Find compatible schemes
799
+ compatible_schemes = get_compatible_schemes_for_credential(credential.auth_type)
800
+
801
+ {
802
+ name: name,
803
+ auth_type: credential.auth_type,
804
+ description: get_credential_description(credential),
805
+ masked_info: get_masked_credential_info(credential),
806
+ url_mappings_count: credential_mappings.size,
807
+ compatible_schemes_count: compatible_schemes.size,
808
+ config_fields: get_credential_config_fields(credential.auth_type)
809
+ }
810
+ end
811
+
812
+ instance_variable_set(:@credentials, credentials_data)
813
+ slim :auth_credentials
814
+ rescue StandardError => e
815
+ logger.error("Error in /auth/credentials route (from AuthenticationRoutes): #{e.class} - #{e.message}")
816
+ halt 500, "Error loading authentication credentials: #{e.message}"
817
+ end
818
+
819
+ # GET /auth/credentials/:name - Individual credential details and configuration
820
+ app.get '/auth/credentials/:name' do
821
+ logger.info("GET /auth/credentials/#{params[:name]} route handler entered (from AuthenticationRoutes)")
822
+ content_type :html
823
+
824
+ credential_name = params[:name].to_sym
825
+ auth_manager = Legate::Auth::Manager.instance
826
+ credential = auth_manager.get_credential(credential_name)
827
+
828
+ halt 404, "Credential not found: #{Rack::Utils.escape_html(params[:name])}" unless credential
829
+
830
+ url_mappings = auth_manager.url_mappings || []
831
+
832
+ # Find URL mappings using this credential
833
+ credential_mappings = url_mappings.select { |mapping| mapping[:credential_name] == credential_name }
834
+ .map.with_index do |mapping, index|
835
+ {
836
+ id: index,
837
+ pattern: mapping[:pattern].is_a?(Regexp) ? mapping[:pattern].source : mapping[:pattern].to_s,
838
+ pattern_type: mapping[:pattern].is_a?(Regexp) ? 'regex' : 'string',
839
+ scheme_name: mapping[:scheme_name]
840
+ }
841
+ end
842
+
843
+ # Find compatible schemes
844
+ compatible_schemes = get_compatible_schemes_for_credential(credential.auth_type)
845
+
846
+ credential_data = {
847
+ name: credential_name,
848
+ auth_type: credential.auth_type,
849
+ description: get_credential_description(credential),
850
+ config_fields: get_credential_config_fields(credential.auth_type),
851
+ current_config: get_credential_current_config(credential),
852
+ compatible_schemes: compatible_schemes,
853
+ url_mappings: credential_mappings
854
+ }
855
+
856
+ instance_variable_set(:@credential, credential_data)
857
+ slim :auth_credential_detail
858
+ rescue StandardError => e
859
+ logger.error("Error in /auth/credentials/#{params[:name]} route (from AuthenticationRoutes): #{e.class} - #{e.message}")
860
+ halt 500, "Error loading credential details: #{e.message}"
861
+ end
862
+
863
+ # POST /auth/credentials - Create new credential
864
+ app.post '/auth/credentials' do
865
+ logger.info('POST /auth/credentials route handler entered (from AuthenticationRoutes)')
866
+ content_type :json
867
+
868
+ auth_type = params[:auth_type]&.to_sym
869
+ credential_name = params[:credential_name]&.to_sym
870
+
871
+ halt 400, { error: 'Credential type is required' }.to_json unless auth_type
872
+ halt 400, { error: 'Credential name is required' }.to_json unless credential_name
873
+
874
+ auth_manager = Legate::Auth::Manager.instance
875
+
876
+ # Check if credential name already exists
877
+ halt 400, { error: "Credential with name '#{credential_name}' already exists" }.to_json if auth_manager.get_credential(credential_name)
878
+
879
+ begin
880
+ # Build credential attributes based on type
881
+ credential_attrs = { auth_type: auth_type }
882
+
883
+ case auth_type
884
+ when :api_key
885
+ credential_attrs[:api_key] = params[:api_key]
886
+ credential_attrs[:location] = params[:location] if params[:location] && !params[:location].empty?
887
+ credential_attrs[:name] = params[:name] if params[:name] && !params[:name].empty?
888
+ when :http_bearer
889
+ credential_attrs[:bearer_token] = params[:bearer_token]
890
+ when :oauth2, :oidc
891
+ credential_attrs[:client_id] = params[:client_id]
892
+ credential_attrs[:client_secret] = params[:client_secret]
893
+ credential_attrs[:redirect_uri] = params[:redirect_uri] if params[:redirect_uri] && !params[:redirect_uri].empty?
894
+ credential_attrs[:scopes] = params[:scopes] if params[:scopes] && !params[:scopes].empty?
895
+ when :service_account
896
+ credential_attrs[:client_email] = params[:client_email]
897
+ credential_attrs[:private_key] = params[:private_key]
898
+ credential_attrs[:project_id] = params[:project_id] if params[:project_id] && !params[:project_id].empty?
899
+ when :google_service_account
900
+ credential_attrs[:service_account_key] = params[:service_account_key]
901
+ when :basic
902
+ credential_attrs[:username] = params[:username]
903
+ credential_attrs[:password] = params[:password]
904
+ else
905
+ halt 400, { error: "Unsupported credential type: #{auth_type}" }.to_json
906
+ end
907
+
908
+ # Create new credential
909
+ credential = Legate::Auth::Credential.new(**credential_attrs)
910
+
911
+ # Register the new credential
912
+ auth_manager.register_credential(credential, credential_name)
913
+
914
+ logger.info("Successfully registered new credential '#{credential_name}' of type '#{auth_type}'")
915
+ { success: true, message: "Credential '#{credential_name}' created successfully" }.to_json
916
+ rescue Legate::Auth::CredentialError => e
917
+ logger.error("Credential validation error for '#{credential_name}': #{e.message}")
918
+ halt 400, { error: "Invalid credential: #{e.message}" }.to_json
919
+ rescue StandardError => e
920
+ logger.error("Error creating credential '#{credential_name}': #{e.class} - #{e.message}")
921
+ halt 500, { error: "Failed to create credential: #{e.message}" }.to_json
922
+ end
923
+ end
924
+
925
+ # PUT /auth/credentials/:name - Update existing credential
926
+ app.put '/auth/credentials/:name' do
927
+ logger.info("PUT /auth/credentials/#{params[:name]} route handler entered (from AuthenticationRoutes)")
928
+ content_type :json
929
+
930
+ credential_name = params[:name].to_sym
931
+ auth_manager = Legate::Auth::Manager.instance
932
+ existing_credential = auth_manager.get_credential(credential_name)
933
+
934
+ halt 404, { error: "Credential not found: #{params[:name]}" }.to_json unless existing_credential
935
+
936
+ begin
937
+ # Build updated credential attributes
938
+ auth_type = existing_credential.auth_type
939
+ credential_attrs = { auth_type: auth_type }
940
+
941
+ case auth_type
942
+ when :api_key
943
+ credential_attrs[:api_key] = params[:api_key]
944
+ credential_attrs[:location] = params[:location] if params[:location] && !params[:location].empty?
945
+ credential_attrs[:name] = params[:name] if params[:name] && !params[:name].empty?
946
+ when :http_bearer
947
+ credential_attrs[:bearer_token] = params[:bearer_token]
948
+ when :oauth2, :oidc
949
+ credential_attrs[:client_id] = params[:client_id]
950
+ credential_attrs[:client_secret] = params[:client_secret]
951
+ credential_attrs[:redirect_uri] = params[:redirect_uri] if params[:redirect_uri] && !params[:redirect_uri].empty?
952
+ credential_attrs[:scopes] = params[:scopes] if params[:scopes] && !params[:scopes].empty?
953
+ when :service_account
954
+ credential_attrs[:client_email] = params[:client_email]
955
+ credential_attrs[:private_key] = params[:private_key]
956
+ credential_attrs[:project_id] = params[:project_id] if params[:project_id] && !params[:project_id].empty?
957
+ when :google_service_account
958
+ credential_attrs[:service_account_key] = params[:service_account_key]
959
+ when :basic
960
+ credential_attrs[:username] = params[:username]
961
+ credential_attrs[:password] = params[:password]
962
+ else
963
+ halt 400, { error: "Credential type '#{auth_type}' does not support updates" }.to_json
964
+ end
965
+
966
+ # Create updated credential
967
+ updated_credential = Legate::Auth::Credential.new(**credential_attrs)
968
+
969
+ # Replace the existing credential
970
+ auth_manager.register_credential(updated_credential, credential_name)
971
+
972
+ logger.info("Successfully updated credential '#{credential_name}'")
973
+ { success: true, message: "Credential '#{credential_name}' updated successfully" }.to_json
974
+ rescue Legate::Auth::CredentialError => e
975
+ logger.error("Credential validation error for '#{credential_name}': #{e.message}")
976
+ halt 400, { error: "Invalid credential: #{e.message}" }.to_json
977
+ rescue StandardError => e
978
+ logger.error("Error updating credential '#{credential_name}': #{e.class} - #{e.message}")
979
+ halt 500, { error: "Failed to update credential: #{e.message}" }.to_json
980
+ end
981
+ end
982
+
983
+ # DELETE /auth/credentials/:name - Remove credential
984
+ app.delete '/auth/credentials/:name' do
985
+ logger.info("DELETE /auth/credentials/#{params[:name]} route handler entered (from AuthenticationRoutes)")
986
+ content_type :json
987
+
988
+ credential_name = params[:name].to_sym
989
+ auth_manager = Legate::Auth::Manager.instance
990
+ credential = auth_manager.get_credential(credential_name)
991
+
992
+ halt 404, { error: "Credential not found: #{params[:name]}" }.to_json unless credential
993
+
994
+ # Check if credential is used in URL mappings
995
+ url_mappings = auth_manager.url_mappings || []
996
+ dependent_mappings = url_mappings.select { |mapping| mapping[:credential_name] == credential_name }
997
+
998
+ if dependent_mappings.any?
999
+ mapping_patterns = dependent_mappings.map { |m| m[:pattern] }.join(', ')
1000
+ halt 400, {
1001
+ error: "Cannot delete credential '#{credential_name}' - it is used by URL mappings: #{mapping_patterns}"
1002
+ }.to_json
1003
+ end
1004
+
1005
+ begin
1006
+ # Remove the credential from the manager (persists via the store)
1007
+ auth_manager.unregister_credential(credential_name)
1008
+
1009
+ logger.info("Successfully deleted credential '#{credential_name}'")
1010
+ { success: true, message: "Credential '#{credential_name}' deleted successfully" }.to_json
1011
+ rescue StandardError => e
1012
+ logger.error("Error deleting credential '#{credential_name}': #{e.class} - #{e.message}")
1013
+ halt 500, { error: "Failed to delete credential: #{e.message}" }.to_json
1014
+ end
1015
+ end
1016
+
1017
+ # POST /auth/credentials/:name/test - Test credential validity
1018
+ app.post '/auth/credentials/:name/test' do
1019
+ logger.info("POST /auth/credentials/#{params[:name]}/test route handler entered (from AuthenticationRoutes)")
1020
+ content_type :json
1021
+
1022
+ credential_name = params[:name].to_sym
1023
+ auth_manager = Legate::Auth::Manager.instance
1024
+ credential = auth_manager.get_credential(credential_name)
1025
+
1026
+ halt 404, { error: "Credential not found: #{params[:name]}" }.to_json unless credential
1027
+
1028
+ begin
1029
+ # Basic validation - check if credential can resolve environment variables
1030
+ test_result = { success: true, tests: [] }
1031
+
1032
+ # Test 1: Environment variable resolution
1033
+ begin
1034
+ credential.to_h(resolve_env: true)
1035
+ test_result[:tests] << {
1036
+ name: 'Environment Variable Resolution',
1037
+ status: 'passed',
1038
+ message: 'All environment variables resolved successfully'
1039
+ }
1040
+ rescue Legate::Auth::EnvironmentVariableNotFoundError => e
1041
+ test_result[:success] = false
1042
+ test_result[:tests] << {
1043
+ name: 'Environment Variable Resolution',
1044
+ status: 'failed',
1045
+ message: "Environment variable not found: #{e.message}"
1046
+ }
1047
+ end
1048
+
1049
+ # Test 2: Required fields validation
1050
+ begin
1051
+ # Create a new credential with the same attributes to validate
1052
+ Legate::Auth::Credential.new(**credential.to_h(resolve_env: false))
1053
+ test_result[:tests] << {
1054
+ name: 'Required Fields Validation',
1055
+ status: 'passed',
1056
+ message: 'All required fields are present'
1057
+ }
1058
+ rescue Legate::Auth::CredentialError => e
1059
+ test_result[:success] = false
1060
+ test_result[:tests] << {
1061
+ name: 'Required Fields Validation',
1062
+ status: 'failed',
1063
+ message: e.message
1064
+ }
1065
+ end
1066
+
1067
+ # Test 3: Format validation for specific types
1068
+ case credential.auth_type
1069
+ when :google_service_account
1070
+ begin
1071
+ key_data = credential[:service_account_key]
1072
+ if key_data
1073
+ JSON.parse(key_data)
1074
+ test_result[:tests] << {
1075
+ name: 'Service Account Key Format',
1076
+ status: 'passed',
1077
+ message: 'Service account key is valid JSON'
1078
+ }
1079
+ end
1080
+ rescue JSON::ParserError
1081
+ test_result[:success] = false
1082
+ test_result[:tests] << {
1083
+ name: 'Service Account Key Format',
1084
+ status: 'failed',
1085
+ message: 'Service account key is not valid JSON'
1086
+ }
1087
+ end
1088
+ when :service_account
1089
+ begin
1090
+ private_key = credential[:private_key]
1091
+ if private_key && !private_key.include?('BEGIN PRIVATE KEY')
1092
+ test_result[:success] = false
1093
+ test_result[:tests] << {
1094
+ name: 'Private Key Format',
1095
+ status: 'failed',
1096
+ message: 'Private key does not appear to be in PEM format'
1097
+ }
1098
+ else
1099
+ test_result[:tests] << {
1100
+ name: 'Private Key Format',
1101
+ status: 'passed',
1102
+ message: 'Private key appears to be in correct PEM format'
1103
+ }
1104
+ end
1105
+ rescue StandardError => e
1106
+ test_result[:success] = false
1107
+ test_result[:tests] << {
1108
+ name: 'Private Key Format',
1109
+ status: 'failed',
1110
+ message: "Private key validation error: #{e.message}"
1111
+ }
1112
+ end
1113
+ end
1114
+
1115
+ logger.info("Credential test completed for '#{credential_name}': #{test_result[:success] ? 'PASSED' : 'FAILED'}")
1116
+ test_result.to_json
1117
+ rescue StandardError => e
1118
+ logger.error("Error testing credential '#{credential_name}': #{e.class} - #{e.message}")
1119
+ halt 500, { error: "Failed to test credential: #{e.message}" }.to_json
1120
+ end
1121
+ end
1122
+
1123
+ # GET /auth/mappings - List all URL mappings
1124
+ app.get '/auth/mappings' do
1125
+ logger.info('GET /auth/mappings route handler entered (from AuthenticationRoutes)')
1126
+ content_type :html
1127
+
1128
+ auth_manager = Legate::Auth::Manager.instance
1129
+ mappings = auth_manager.url_mappings || []
1130
+
1131
+ # Convert mappings to a view-friendly format
1132
+ mappings_data = mappings.map.with_index do |mapping, index|
1133
+ {
1134
+ id: index,
1135
+ pattern: mapping[:pattern].is_a?(Regexp) ? mapping[:pattern].source : mapping[:pattern].to_s,
1136
+ pattern_type: mapping[:pattern].is_a?(Regexp) ? 'regex' : 'string',
1137
+ scheme_name: mapping[:scheme_name],
1138
+ credential_name: mapping[:credential_name]
1139
+ }
1140
+ end
1141
+
1142
+ instance_variable_set(:@mappings, mappings_data)
1143
+ slim :auth_mappings
1144
+ rescue StandardError => e
1145
+ logger.error("Error in /auth/mappings route (from AuthenticationRoutes): #{e.class} - #{e.message}")
1146
+ halt 500, "Error loading URL mappings: #{e.message}"
1147
+ end
1148
+
1149
+ # GET /auth/debug - Debug information about authentication state
1150
+ app.get '/auth/debug' do
1151
+ logger.info('GET /auth/debug route handler entered (from AuthenticationRoutes)')
1152
+ content_type :html
1153
+
1154
+ auth_manager = Legate::Auth::Manager.instance
1155
+
1156
+ # Gather debug information
1157
+ debug_info = {
1158
+ manager_class: auth_manager.class.name,
1159
+ schemes_registered: auth_manager.schemes&.keys || [],
1160
+ credentials_registered: auth_manager.credentials&.keys || [],
1161
+ url_mappings_count: auth_manager.url_mappings&.size || 0,
1162
+ manager_instance_id: auth_manager.object_id
1163
+ }
1164
+
1165
+ instance_variable_set(:@debug_info, debug_info)
1166
+ slim :auth_debug
1167
+ rescue StandardError => e
1168
+ logger.error("Error in /auth/debug route (from AuthenticationRoutes): #{e.class} - #{e.message}")
1169
+ halt 500, "Error gathering debug information: #{e.message}"
1170
+ end
1171
+
1172
+ # GET /auth/test - Main testing dashboard
1173
+ app.get '/auth/test' do
1174
+ logger.info('GET /auth/test route handler entered (from AuthenticationRoutes)')
1175
+ content_type :html
1176
+
1177
+ auth_manager = Legate::Auth::Manager.instance
1178
+ schemes = auth_manager.schemes || {}
1179
+ credentials = auth_manager.credentials || {}
1180
+ mappings = auth_manager.url_mappings || []
1181
+
1182
+ # Convert to view-friendly format
1183
+ schemes_data = schemes.map do |name, scheme|
1184
+ {
1185
+ name: name,
1186
+ scheme_type: scheme.scheme_type,
1187
+ description: get_scheme_description(scheme)
1188
+ }
1189
+ end
1190
+
1191
+ credentials_data = credentials.map do |name, credential|
1192
+ {
1193
+ name: name,
1194
+ auth_type: credential.auth_type,
1195
+ description: get_credential_description(credential)
1196
+ }
1197
+ end
1198
+
1199
+ instance_variable_set(:@schemes, schemes_data)
1200
+ instance_variable_set(:@credentials, credentials_data)
1201
+ instance_variable_set(:@mappings_count, mappings.size)
1202
+ slim :auth_test
1203
+ rescue StandardError => e
1204
+ logger.error("Error in /auth/test route (from AuthenticationRoutes): #{e.class} - #{e.message}")
1205
+ halt 500, "Error loading testing dashboard: #{e.message}"
1206
+ end
1207
+
1208
+ # POST /auth/test/credential/:name - Test individual credential
1209
+ app.post '/auth/test/credential/:name' do
1210
+ logger.info("POST /auth/test/credential/#{params[:name]} route handler entered (from AuthenticationRoutes)")
1211
+ content_type :json
1212
+
1213
+ credential_name = params[:name].to_sym
1214
+ auth_manager = Legate::Auth::Manager.instance
1215
+ credential = auth_manager.get_credential(credential_name)
1216
+
1217
+ halt 404, { error: "Credential not found: #{params[:name]}" }.to_json unless credential
1218
+
1219
+ begin
1220
+ test_options = {}
1221
+ test_options[:test_url] = params[:test_url] if params[:test_url] && !params[:test_url].empty?
1222
+
1223
+ test_results = test_credential_functionality(credential, test_options)
1224
+ test_results[:credential_name] = credential_name.to_s
1225
+
1226
+ logger.info("Credential test completed for '#{credential_name}': #{test_results[:success] ? 'PASSED' : 'FAILED'}")
1227
+ test_results.to_json
1228
+ rescue StandardError => e
1229
+ logger.error("Error testing credential '#{credential_name}': #{e.class} - #{e.message}")
1230
+ halt 500, { error: "Failed to test credential: #{e.message}" }.to_json
1231
+ end
1232
+ end
1233
+
1234
+ # POST /auth/test/scheme/:name - Test authentication scheme
1235
+ app.post '/auth/test/scheme/:name' do
1236
+ logger.info("POST /auth/test/scheme/#{params[:name]} route handler entered (from AuthenticationRoutes)")
1237
+ content_type :json
1238
+
1239
+ scheme_name = params[:name].to_sym
1240
+ auth_manager = Legate::Auth::Manager.instance
1241
+ scheme = auth_manager.get_scheme(scheme_name)
1242
+
1243
+ halt 404, { error: "Scheme not found: #{params[:name]}" }.to_json unless scheme
1244
+
1245
+ begin
1246
+ test_results = { success: true, tests: [], scheme_name: scheme_name.to_s }
1247
+
1248
+ # Test 1: Scheme configuration validation
1249
+ case scheme.scheme_type
1250
+ when :oauth2, :oidc, :openid_connect
1251
+ if scheme.respond_to?(:authorization_url) && scheme.authorization_url
1252
+ test_results[:tests] << {
1253
+ name: 'Authorization URL',
1254
+ status: 'passed',
1255
+ message: "Valid authorization URL: #{scheme.authorization_url}"
1256
+ }
1257
+ else
1258
+ test_results[:success] = false
1259
+ test_results[:tests] << {
1260
+ name: 'Authorization URL',
1261
+ status: 'failed',
1262
+ message: 'Authorization URL is missing or invalid'
1263
+ }
1264
+ end
1265
+
1266
+ if scheme.respond_to?(:token_url) && scheme.token_url
1267
+ test_results[:tests] << {
1268
+ name: 'Token URL',
1269
+ status: 'passed',
1270
+ message: "Valid token URL: #{scheme.token_url}"
1271
+ }
1272
+ else
1273
+ test_results[:success] = false
1274
+ test_results[:tests] << {
1275
+ name: 'Token URL',
1276
+ status: 'failed',
1277
+ message: 'Token URL is missing or invalid'
1278
+ }
1279
+ end
1280
+ when :service_account, :google_service_account
1281
+ test_results[:tests] << if scheme.respond_to?(:token_url) && scheme.token_url
1282
+ {
1283
+ name: 'Token URL',
1284
+ status: 'passed',
1285
+ message: "Valid token URL: #{scheme.token_url}"
1286
+ }
1287
+ else
1288
+ {
1289
+ name: 'Token URL',
1290
+ status: 'skipped',
1291
+ message: 'No token URL configured (using default)'
1292
+ }
1293
+ end
1294
+ else
1295
+ test_results[:tests] << {
1296
+ name: 'Scheme Configuration',
1297
+ status: 'passed',
1298
+ message: "Scheme type #{scheme.scheme_type} requires no additional configuration"
1299
+ }
1300
+ end
1301
+
1302
+ # Test 2: Compatible credentials check
1303
+ credentials = auth_manager.credentials || {}
1304
+ compatible_credentials = credentials.select do |cred_name, credential|
1305
+ mapping_scheme_credential_compatible?(scheme, credential)
1306
+ end
1307
+
1308
+ test_results[:tests] << if compatible_credentials.any?
1309
+ {
1310
+ name: 'Compatible Credentials',
1311
+ status: 'passed',
1312
+ message: "Found #{compatible_credentials.size} compatible credential(s)"
1313
+ }
1314
+ else
1315
+ {
1316
+ name: 'Compatible Credentials',
1317
+ status: 'warning',
1318
+ message: 'No compatible credentials found'
1319
+ }
1320
+ end
1321
+
1322
+ logger.info("Scheme test completed for '#{scheme_name}': #{test_results[:success] ? 'PASSED' : 'FAILED'}")
1323
+ test_results.to_json
1324
+ rescue StandardError => e
1325
+ logger.error("Error testing scheme '#{scheme_name}': #{e.class} - #{e.message}")
1326
+ halt 500, { error: "Failed to test scheme: #{e.message}" }.to_json
1327
+ end
1328
+ end
1329
+
1330
+ # POST /auth/test/api - Test API call with authentication
1331
+ app.post '/auth/test/api' do
1332
+ logger.info('POST /auth/test/api route handler entered (from AuthenticationRoutes)')
1333
+ content_type :json
1334
+
1335
+ url = params[:url]
1336
+ scheme_name = params[:scheme_name]
1337
+ credential_name = params[:credential_name]
1338
+
1339
+ halt 400, { error: 'URL is required' }.to_json unless url && !url.empty?
1340
+ halt 400, { error: 'Scheme name is required' }.to_json unless scheme_name && !scheme_name.empty?
1341
+ halt 400, { error: 'Credential name is required' }.to_json unless credential_name && !credential_name.empty?
1342
+
1343
+ begin
1344
+ result = test_authenticated_api_call(url, scheme_name, credential_name)
1345
+
1346
+ logger.info("API test completed for URL '#{url}' with scheme '#{scheme_name}' and credential '#{credential_name}': #{result[:success] ? 'SUCCESS' : 'FAILED'}")
1347
+ result.to_json
1348
+ rescue StandardError => e
1349
+ logger.error("Error testing API call: #{e.class} - #{e.message}")
1350
+ halt 500, { error: "Failed to test API call: #{e.message}" }.to_json
1351
+ end
1352
+ end
1353
+
1354
+ # POST /auth/test/flow - Test complete authentication flow
1355
+ app.post '/auth/test/flow' do
1356
+ logger.info('POST /auth/test/flow route handler entered (from AuthenticationRoutes)')
1357
+ content_type :json
1358
+
1359
+ scheme_name = params[:scheme_name]
1360
+ credential_name = params[:credential_name]
1361
+
1362
+ halt 400, { error: 'Scheme name is required' }.to_json unless scheme_name && !scheme_name.empty?
1363
+ halt 400, { error: 'Credential name is required' }.to_json unless credential_name && !credential_name.empty?
1364
+
1365
+ begin
1366
+ auth_manager = Legate::Auth::Manager.instance
1367
+ scheme = auth_manager.get_scheme(scheme_name.to_sym)
1368
+ credential = auth_manager.get_credential(credential_name.to_sym)
1369
+
1370
+ halt 404, { error: 'Scheme not found' }.to_json unless scheme
1371
+ halt 404, { error: 'Credential not found' }.to_json unless credential
1372
+ halt 400, { error: 'Scheme and credential are not compatible' }.to_json unless mapping_scheme_credential_compatible?(scheme, credential)
1373
+
1374
+ flow_results = { success: true, steps: [], scheme_type: scheme.scheme_type }
1375
+
1376
+ case scheme.scheme_type
1377
+ when :api_key
1378
+ flow_results[:steps] << {
1379
+ step: 'API Key Authentication',
1380
+ status: 'passed',
1381
+ message: 'API key authentication is ready for use'
1382
+ }
1383
+ when :http_bearer
1384
+ flow_results[:steps] << {
1385
+ step: 'Bearer Token Authentication',
1386
+ status: 'passed',
1387
+ message: 'Bearer token authentication is ready for use'
1388
+ }
1389
+ when :oauth2, :oidc, :openid_connect
1390
+ flow_results[:steps] << {
1391
+ step: 'OAuth2 Flow Simulation',
1392
+ status: 'simulated',
1393
+ message: 'OAuth2 flow would redirect to authorization URL and exchange code for token'
1394
+ }
1395
+ when :service_account, :google_service_account
1396
+ flow_results[:steps] << {
1397
+ step: 'Service Account Flow Simulation',
1398
+ status: 'simulated',
1399
+ message: 'Service account would generate JWT and exchange for access token'
1400
+ }
1401
+ else
1402
+ flow_results[:success] = false
1403
+ flow_results[:steps] << {
1404
+ step: 'Unknown Flow',
1405
+ status: 'failed',
1406
+ message: "No flow simulation available for scheme type: #{scheme.scheme_type}"
1407
+ }
1408
+ end
1409
+
1410
+ logger.info("Flow test completed for scheme '#{scheme_name}' and credential '#{credential_name}': #{flow_results[:success] ? 'SUCCESS' : 'FAILED'}")
1411
+ flow_results.to_json
1412
+ rescue StandardError => e
1413
+ logger.error("Error testing authentication flow: #{e.class} - #{e.message}")
1414
+ halt 500, { error: "Failed to test authentication flow: #{e.message}" }.to_json
1415
+ end
1416
+ end
1417
+
1418
+ # GET /auth/mappings/new - Form for creating new mapping
1419
+ app.get '/auth/mappings/new' do
1420
+ auth_manager = Legate::Auth::Manager.instance
1421
+ schemes = auth_manager.schemes || {}
1422
+ credentials = auth_manager.credentials || {}
1423
+ mapping_fields = get_mapping_config_fields
1424
+ instance_variable_set(:@schemes, schemes)
1425
+ instance_variable_set(:@credentials, credentials)
1426
+ instance_variable_set(:@mapping_fields, mapping_fields)
1427
+ slim :auth_mapping_new
1428
+ end
1429
+
1430
+ # GET /auth/mappings/:id - Mapping detail/edit
1431
+ app.get '/auth/mappings/:id' do
1432
+ auth_manager = Legate::Auth::Manager.instance
1433
+ mappings = auth_manager.url_mappings || []
1434
+ id = params[:id].to_i
1435
+ mapping = mappings[id] or halt 404, 'Mapping not found'
1436
+ schemes = auth_manager.schemes || {}
1437
+ credentials = auth_manager.credentials || {}
1438
+ mapping_fields = get_mapping_config_fields
1439
+ instance_variable_set(:@mapping, mapping)
1440
+ instance_variable_set(:@schemes, schemes)
1441
+ instance_variable_set(:@credentials, credentials)
1442
+ instance_variable_set(:@mapping_fields, mapping_fields)
1443
+ slim :auth_mapping_detail
1444
+ end
1445
+
1446
+ # POST /auth/mappings - Create new mapping
1447
+ app.post '/auth/mappings' do
1448
+ content_type :json
1449
+ auth_manager = Legate::Auth::Manager.instance
1450
+ mappings = auth_manager.url_mappings || []
1451
+ pattern = params[:pattern]
1452
+ pattern_type = params[:pattern_type]
1453
+ scheme_name = params[:scheme_name]&.to_sym
1454
+ credential_name = params[:credential_name]&.to_sym
1455
+ priority = params[:priority]&.to_i || 1
1456
+ active = %w[on true].include?(params[:active])
1457
+ # Validate pattern
1458
+ halt 400, { error: 'Invalid pattern' }.to_json unless valid_mapping_pattern?(pattern, pattern_type)
1459
+ # Validate scheme/credential
1460
+ scheme = auth_manager.get_scheme(scheme_name)
1461
+ credential = auth_manager.get_credential(credential_name)
1462
+ halt 400, { error: 'Scheme and credential are not compatible' }.to_json unless mapping_scheme_credential_compatible?(scheme, credential)
1463
+ # Build mapping
1464
+ mapping = {
1465
+ pattern: pattern_type == 'regex' ? Regexp.new(pattern) : pattern,
1466
+ pattern_type: pattern_type,
1467
+ scheme_name: scheme_name,
1468
+ credential_name: credential_name,
1469
+ priority: priority,
1470
+ active: active
1471
+ }
1472
+ mappings << mapping
1473
+ auth_manager.replace_url_mappings(mappings)
1474
+ { success: true, message: 'Mapping created', id: mappings.size - 1 }.to_json
1475
+ end
1476
+
1477
+ # PUT /auth/mappings/:id - Update mapping
1478
+ app.put '/auth/mappings/:id' do
1479
+ content_type :json
1480
+ auth_manager = Legate::Auth::Manager.instance
1481
+ mappings = auth_manager.url_mappings || []
1482
+ id = params[:id].to_i
1483
+ mapping = mappings[id] or halt 404, { error: 'Mapping not found' }.to_json
1484
+ pattern = params[:pattern]
1485
+ pattern_type = params[:pattern_type]
1486
+ scheme_name = params[:scheme_name]&.to_sym
1487
+ credential_name = params[:credential_name]&.to_sym
1488
+ priority = params[:priority]&.to_i || 1
1489
+ active = %w[on true].include?(params[:active])
1490
+ halt 400, { error: 'Invalid pattern' }.to_json unless valid_mapping_pattern?(pattern, pattern_type)
1491
+ scheme = auth_manager.get_scheme(scheme_name)
1492
+ credential = auth_manager.get_credential(credential_name)
1493
+ halt 400, { error: 'Scheme and credential are not compatible' }.to_json unless mapping_scheme_credential_compatible?(scheme, credential)
1494
+ mapping[:pattern] = pattern_type == 'regex' ? Regexp.new(pattern) : pattern
1495
+ mapping[:pattern_type] = pattern_type
1496
+ mapping[:scheme_name] = scheme_name
1497
+ mapping[:credential_name] = credential_name
1498
+ mapping[:priority] = priority
1499
+ mapping[:active] = active
1500
+ auth_manager.replace_url_mappings(mappings)
1501
+ { success: true, message: 'Mapping updated' }.to_json
1502
+ end
1503
+
1504
+ # DELETE /auth/mappings/:id - Remove mapping
1505
+ app.delete '/auth/mappings/:id' do
1506
+ content_type :json
1507
+ auth_manager = Legate::Auth::Manager.instance
1508
+ mappings = auth_manager.url_mappings || []
1509
+ id = params[:id].to_i
1510
+ mapping = mappings[id] or halt 404, { error: 'Mapping not found' }.to_json
1511
+ mappings.delete_at(id)
1512
+ auth_manager.replace_url_mappings(mappings)
1513
+ { success: true, message: 'Mapping deleted' }.to_json
1514
+ end
1515
+
1516
+ # POST /auth/mappings/test - Test pattern matching
1517
+ app.post '/auth/mappings/test' do
1518
+ content_type :json
1519
+ pattern = params[:pattern]
1520
+ pattern_type = params[:pattern_type]
1521
+ url = params[:url]
1522
+ halt 400, { error: 'Invalid pattern' }.to_json unless valid_mapping_pattern?(pattern, pattern_type)
1523
+ begin
1524
+ matched =
1525
+ if pattern_type == 'regex'
1526
+ !!(url =~ Regexp.new(pattern))
1527
+ elsif pattern.include?('*')
1528
+ regex = Regexp.new('^' + Regexp.escape(pattern).gsub('\\*', '.*') + '$')
1529
+ !!(url =~ regex)
1530
+ else
1531
+ url == pattern
1532
+ end
1533
+ { success: true, matched: matched }.to_json
1534
+ rescue StandardError => e
1535
+ halt 400, { error: "Pattern test error: #{e.message}" }.to_json
1536
+ end
1537
+ end
1538
+ end
1539
+ end
1540
+ end
1541
+ end