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,394 @@
1
+ # File: lib/legate/auth/manager_store.rb
2
+ # frozen_string_literal: true
3
+
4
+ require 'json'
5
+
6
+ module Legate
7
+ module Auth
8
+ # Module for persisting authentication configuration (schemes, credentials, URL mappings)
9
+ module ManagerStore
10
+ # Redis-backed implementation for storing authentication configuration
11
+ class RedisStore
12
+ # Redis key prefixes
13
+ SCHEMES_HASH_KEY = 'legate:auth:schemes'
14
+ CREDENTIALS_HASH_KEY = 'legate:auth:credentials'
15
+ URL_MAPPINGS_KEY = 'legate:auth:url_mappings'
16
+
17
+ # @param redis_client [Redis] An instance of the Redis client
18
+ def initialize(redis_client:)
19
+ @redis = redis_client
20
+ @logger = Legate.logger
21
+ @logger&.info('Legate::Auth::ManagerStore::RedisStore initialized.')
22
+ rescue StandardError => e
23
+ Legate.logger&.error("Failed to initialize Auth ManagerStore: #{e.message}")
24
+ @redis = nil
25
+ end
26
+
27
+ # Check if Redis is available
28
+ # @return [Boolean]
29
+ def available?
30
+ !@redis.nil?
31
+ end
32
+
33
+ # ========== Scheme Operations ==========
34
+
35
+ # Save a scheme configuration
36
+ # @param name [String, Symbol] The scheme name
37
+ # @param scheme [Legate::Auth::Scheme] The scheme to save
38
+ # @return [Boolean] true if successful
39
+ def save_scheme(name, scheme)
40
+ return false unless available?
41
+
42
+ name = name.to_s
43
+
44
+ scheme_data = serialize_scheme(scheme)
45
+ @redis.hset(SCHEMES_HASH_KEY, name, scheme_data.to_json)
46
+ @logger&.debug("Saved auth scheme '#{name}' to Redis")
47
+ true
48
+ rescue StandardError => e
49
+ @logger&.error("Failed to save scheme '#{name}': #{e.message}")
50
+ false
51
+ end
52
+
53
+ # Load a single scheme
54
+ # @param name [String, Symbol] The scheme name
55
+ # @return [Hash, nil] The scheme data or nil if not found
56
+ def load_scheme(name)
57
+ return nil unless available?
58
+
59
+ name = name.to_s
60
+ data = @redis.hget(SCHEMES_HASH_KEY, name)
61
+ return nil unless data
62
+
63
+ JSON.parse(data, symbolize_names: true)
64
+ rescue StandardError => e
65
+ @logger&.error("Failed to load scheme '#{name}': #{e.message}")
66
+ nil
67
+ end
68
+
69
+ # Load all schemes
70
+ # @return [Hash] Hash of scheme_name => scheme_data
71
+ def load_all_schemes
72
+ return {} unless available?
73
+
74
+ result = {}
75
+ @redis.hgetall(SCHEMES_HASH_KEY).each do |name, data|
76
+ result[name.to_sym] = JSON.parse(data, symbolize_names: true)
77
+ rescue JSON::ParserError => e
78
+ @logger&.warn("Failed to parse scheme '#{name}': #{e.message}")
79
+ end
80
+ result
81
+ rescue StandardError => e
82
+ @logger&.error("Failed to load schemes: #{e.message}")
83
+ {}
84
+ end
85
+
86
+ # Delete a scheme
87
+ # @param name [String, Symbol] The scheme name
88
+ # @return [Boolean] true if successful
89
+ def delete_scheme(name)
90
+ return false unless available?
91
+
92
+ @redis.hdel(SCHEMES_HASH_KEY, name.to_s)
93
+ @logger&.debug("Deleted auth scheme '#{name}' from Redis")
94
+ true
95
+ rescue StandardError => e
96
+ @logger&.error("Failed to delete scheme '#{name}': #{e.message}")
97
+ false
98
+ end
99
+
100
+ # ========== Credential Operations ==========
101
+
102
+ # Save a credential
103
+ # @param name [String, Symbol] The credential name
104
+ # @param credential [Legate::Auth::Credential] The credential to save
105
+ # @return [Boolean] true if successful
106
+ def save_credential(name, credential)
107
+ return false unless available?
108
+
109
+ name = name.to_s
110
+
111
+ credential_data = serialize_credential(credential)
112
+ @redis.hset(CREDENTIALS_HASH_KEY, name, credential_data.to_json)
113
+ @logger&.debug("Saved auth credential '#{name}' to Redis")
114
+ true
115
+ rescue StandardError => e
116
+ @logger&.error("Failed to save credential '#{name}': #{e.message}")
117
+ false
118
+ end
119
+
120
+ # Load a single credential
121
+ # @param name [String, Symbol] The credential name
122
+ # @return [Hash, nil] The credential data or nil if not found
123
+ def load_credential(name)
124
+ return nil unless available?
125
+
126
+ name = name.to_s
127
+ data = @redis.hget(CREDENTIALS_HASH_KEY, name)
128
+ return nil unless data
129
+
130
+ JSON.parse(data, symbolize_names: true)
131
+ rescue StandardError => e
132
+ @logger&.error("Failed to load credential '#{name}': #{e.message}")
133
+ nil
134
+ end
135
+
136
+ # Load all credentials
137
+ # @return [Hash] Hash of credential_name => credential_data
138
+ def load_all_credentials
139
+ return {} unless available?
140
+
141
+ result = {}
142
+ @redis.hgetall(CREDENTIALS_HASH_KEY).each do |name, data|
143
+ result[name.to_sym] = JSON.parse(data, symbolize_names: true)
144
+ rescue JSON::ParserError => e
145
+ @logger&.warn("Failed to parse credential '#{name}': #{e.message}")
146
+ end
147
+ result
148
+ rescue StandardError => e
149
+ @logger&.error("Failed to load credentials: #{e.message}")
150
+ {}
151
+ end
152
+
153
+ # Delete a credential
154
+ # @param name [String, Symbol] The credential name
155
+ # @return [Boolean] true if successful
156
+ def delete_credential(name)
157
+ return false unless available?
158
+
159
+ @redis.hdel(CREDENTIALS_HASH_KEY, name.to_s)
160
+ @logger&.debug("Deleted auth credential '#{name}' from Redis")
161
+ true
162
+ rescue StandardError => e
163
+ @logger&.error("Failed to delete credential '#{name}': #{e.message}")
164
+ false
165
+ end
166
+
167
+ # ========== URL Mapping Operations ==========
168
+
169
+ # Save URL mappings (replaces all)
170
+ # @param mappings [Array<Hash>] Array of URL mapping hashes
171
+ # @return [Boolean] true if successful
172
+ def save_url_mappings(mappings)
173
+ return false unless available?
174
+
175
+ serialized = mappings.map do |mapping|
176
+ {
177
+ pattern: mapping[:pattern].is_a?(Regexp) ? { regexp: mapping[:pattern].source } : mapping[:pattern],
178
+ scheme_name: mapping[:scheme_name].to_s,
179
+ credential_name: mapping[:credential_name].to_s
180
+ }
181
+ end
182
+
183
+ @redis.set(URL_MAPPINGS_KEY, serialized.to_json)
184
+ @logger&.debug("Saved #{mappings.size} URL mappings to Redis")
185
+ true
186
+ rescue StandardError => e
187
+ @logger&.error("Failed to save URL mappings: #{e.message}")
188
+ false
189
+ end
190
+
191
+ # Load URL mappings
192
+ # @return [Array<Hash>] Array of URL mapping hashes
193
+ def load_url_mappings
194
+ return [] unless available?
195
+
196
+ data = @redis.get(URL_MAPPINGS_KEY)
197
+ return [] unless data
198
+
199
+ mappings = JSON.parse(data, symbolize_names: true)
200
+
201
+ # Reconstruct Regexp patterns
202
+ mappings.map do |mapping|
203
+ pattern = mapping[:pattern]
204
+ pattern = Regexp.new(pattern[:regexp]) if pattern.is_a?(Hash) && pattern[:regexp]
205
+
206
+ {
207
+ pattern: pattern,
208
+ scheme_name: mapping[:scheme_name].to_sym,
209
+ credential_name: mapping[:credential_name].to_sym
210
+ }
211
+ end
212
+ rescue StandardError => e
213
+ @logger&.error("Failed to load URL mappings: #{e.message}")
214
+ []
215
+ end
216
+
217
+ # Add a single URL mapping
218
+ # @param mapping [Hash] The URL mapping to add
219
+ # @return [Boolean] true if successful
220
+ def add_url_mapping(mapping)
221
+ return false unless available?
222
+
223
+ current = load_url_mappings
224
+ current << mapping
225
+ save_url_mappings(current)
226
+ end
227
+
228
+ # Remove a URL mapping
229
+ # @param index [Integer] The index of the mapping to remove
230
+ # @return [Boolean] true if successful
231
+ def remove_url_mapping(index)
232
+ return false unless available?
233
+
234
+ current = load_url_mappings
235
+ return false if index < 0 || index >= current.size
236
+
237
+ current.delete_at(index)
238
+ save_url_mappings(current)
239
+ end
240
+
241
+ # Clear all URL mappings
242
+ # @return [Boolean] true if successful
243
+ def clear_url_mappings
244
+ return false unless available?
245
+
246
+ @redis.del(URL_MAPPINGS_KEY)
247
+ true
248
+ rescue StandardError => e
249
+ @logger&.error("Failed to clear URL mappings: #{e.message}")
250
+ false
251
+ end
252
+
253
+ private
254
+
255
+ # Serialize a scheme to a storable hash
256
+ # @param scheme [Legate::Auth::Scheme] The scheme
257
+ # @return [Hash] Serialized scheme data
258
+ def serialize_scheme(scheme)
259
+ data = {
260
+ scheme_type: scheme.scheme_type.to_s,
261
+ class_name: scheme.class.name
262
+ }
263
+
264
+ # Add scheme-specific configuration
265
+ case scheme
266
+ when Legate::Auth::Schemes::OAuth2, Legate::Auth::Schemes::OpenIDConnect
267
+ data[:authorization_url] = scheme.authorization_url if scheme.respond_to?(:authorization_url)
268
+ data[:token_url] = scheme.token_url if scheme.respond_to?(:token_url)
269
+ data[:scopes] = scheme.scopes if scheme.respond_to?(:scopes)
270
+ data[:use_pkce] = scheme.use_pkce if scheme.respond_to?(:use_pkce)
271
+ data[:revocation_url] = scheme.revocation_url if scheme.respond_to?(:revocation_url)
272
+ when Legate::Auth::Schemes::ApiKey
273
+ data[:header_name] = scheme.header_name if scheme.respond_to?(:header_name)
274
+ data[:query_param_name] = scheme.query_param_name if scheme.respond_to?(:query_param_name)
275
+ data[:location] = scheme.location if scheme.respond_to?(:location)
276
+ when Legate::Auth::Schemes::ServiceAccount
277
+ data[:token_url] = scheme.token_url if scheme.respond_to?(:token_url)
278
+ when Legate::Auth::Schemes::GoogleServiceAccount
279
+ data[:scopes] = scheme.scopes if scheme.respond_to?(:scopes)
280
+ end
281
+
282
+ data
283
+ end
284
+
285
+ # Serialize a credential to a storable hash
286
+ # @param credential [Legate::Auth::Credential] The credential
287
+ # @return [Hash] Serialized credential data
288
+ def serialize_credential(credential)
289
+ data = {
290
+ auth_type: credential.auth_type.to_s
291
+ }
292
+
293
+ # Copy all attributes (they may contain ENV: references which should be preserved as-is)
294
+ attributes = credential.instance_variable_get(:@attributes) || {}
295
+ attributes.each do |key, value|
296
+ data[key] = value
297
+ end
298
+
299
+ data
300
+ end
301
+ end
302
+
303
+ # In-memory fallback store (for when Redis is unavailable)
304
+ class InMemoryStore
305
+ def initialize
306
+ @schemes = {}
307
+ @credentials = {}
308
+ @url_mappings = []
309
+ @logger = Legate.logger
310
+ @logger&.info('Legate::Auth::ManagerStore::InMemoryStore initialized (no persistence).')
311
+ end
312
+
313
+ def available?
314
+ true
315
+ end
316
+
317
+ def save_scheme(name, scheme)
318
+ @schemes[name.to_sym] = serialize_scheme(scheme)
319
+ true
320
+ end
321
+
322
+ def load_scheme(name)
323
+ @schemes[name.to_sym]
324
+ end
325
+
326
+ def load_all_schemes
327
+ @schemes.dup
328
+ end
329
+
330
+ def delete_scheme(name)
331
+ @schemes.delete(name.to_sym)
332
+ true
333
+ end
334
+
335
+ def save_credential(name, credential)
336
+ @credentials[name.to_sym] = serialize_credential(credential)
337
+ true
338
+ end
339
+
340
+ def load_credential(name)
341
+ @credentials[name.to_sym]
342
+ end
343
+
344
+ def load_all_credentials
345
+ @credentials.dup
346
+ end
347
+
348
+ def delete_credential(name)
349
+ @credentials.delete(name.to_sym)
350
+ true
351
+ end
352
+
353
+ def save_url_mappings(mappings)
354
+ @url_mappings = mappings.dup
355
+ true
356
+ end
357
+
358
+ def load_url_mappings
359
+ @url_mappings.dup
360
+ end
361
+
362
+ def add_url_mapping(mapping)
363
+ @url_mappings << mapping
364
+ true
365
+ end
366
+
367
+ def remove_url_mapping(index)
368
+ return false if index < 0 || index >= @url_mappings.size
369
+
370
+ @url_mappings.delete_at(index)
371
+ true
372
+ end
373
+
374
+ def clear_url_mappings
375
+ @url_mappings = []
376
+ true
377
+ end
378
+
379
+ private
380
+
381
+ def serialize_scheme(scheme)
382
+ { scheme_type: scheme.scheme_type.to_s, class_name: scheme.class.name }
383
+ end
384
+
385
+ def serialize_credential(credential)
386
+ data = { auth_type: credential.auth_type.to_s }
387
+ attributes = credential.instance_variable_get(:@attributes) || {}
388
+ attributes.each { |k, v| data[k] = v }
389
+ data
390
+ end
391
+ end
392
+ end
393
+ end
394
+ end
@@ -0,0 +1,290 @@
1
+ # File: lib/legate/auth/middleware_factory.rb
2
+ # frozen_string_literal: true
3
+
4
+ require_relative 'excon_middleware'
5
+ require_relative 'token_store'
6
+ require_relative 'token_manager'
7
+ require_relative 'schemes/api_key'
8
+ require_relative 'schemes/http_bearer'
9
+ require_relative 'schemes/oauth2'
10
+ require_relative 'schemes/openid_connect'
11
+ require_relative 'schemes/service_account'
12
+
13
+ module Legate
14
+ module Auth
15
+ # Factory for creating authentication middleware instances
16
+ # based on different scheme types and configurations.
17
+ class MiddlewareFactory
18
+ class << self
19
+ # Create middleware for any authentication scheme
20
+ # @param scheme [Legate::Auth::Scheme] The authentication scheme to use
21
+ # @param credential [Legate::Auth::Credential] The credential to use
22
+ # @param options [Hash] Additional options for the middleware
23
+ # @option options [Legate::Auth::TokenStore] :token_store Optional token store for caching tokens
24
+ # @option options [Legate::Auth::TokenManager] :token_manager Optional token manager for token lifecycle
25
+ # @option options [Boolean] :auto_retry Whether to automatically retry on auth errors (default: true)
26
+ # @option options [Integer] :max_retries Maximum number of retries (default: 3)
27
+ # @option options [Symbol] :backoff_strategy Strategy for retries (:linear, :exponential, :fibonacci, :jitter, :none)
28
+ # @option options [Float] :backoff_factor Factor to use for backoff calculation (default: 1.0)
29
+ # @option options [Boolean] :retry_non_idempotent Whether to retry non-idempotent requests (default: false)
30
+ # @option options [Array<Integer>] :retry_on Additional HTTP status codes to retry on
31
+ # @return [Legate::Auth::ExconMiddleware] The configured middleware
32
+ def create(scheme:, credential:, **options)
33
+ # Create a token store if not provided
34
+ token_store = options[:token_store]
35
+ unless token_store
36
+ session_service = options[:session_service]
37
+ token_store = Legate::Auth::TokenStore.new(session_service) if session_service
38
+ end
39
+
40
+ # Create a token manager if not provided
41
+ token_manager = options[:token_manager]
42
+
43
+ # Configure retry options
44
+ auto_retry = options.key?(:auto_retry) ? options[:auto_retry] : true
45
+ max_retries = options[:max_retries] || 3
46
+ backoff_strategy = options[:backoff_strategy] || :exponential
47
+ backoff_factor = options[:backoff_factor] || 1.0
48
+ retry_non_idempotent = options[:retry_non_idempotent] || false
49
+ retry_on = options[:retry_on] || []
50
+
51
+ # Create the middleware instance with nil stack (will be set by Excon later)
52
+ Legate::Auth::ExconMiddleware.new(nil, {
53
+ scheme: scheme,
54
+ credential: credential,
55
+ token_store: token_store,
56
+ token_manager: token_manager,
57
+ auto_retry: auto_retry,
58
+ max_retries: max_retries,
59
+ backoff_strategy: backoff_strategy,
60
+ backoff_factor: backoff_factor,
61
+ retry_non_idempotent: retry_non_idempotent,
62
+ retry_on: retry_on
63
+ })
64
+ end
65
+
66
+ # Create middleware specifically for API key authentication
67
+ # @param api_key [String] The API key to use
68
+ # @param location [String] Where to place the API key ('header', 'query', 'cookie')
69
+ # @param name [String] The name of the parameter/header
70
+ # @param options [Hash] Additional options for the middleware
71
+ # @return [Legate::Auth::ExconMiddleware] The configured middleware
72
+ def create_api_key(api_key:, location: 'header', name: 'X-API-Key', **options)
73
+ # Create the scheme
74
+ scheme = Legate::Auth::Schemes::ApiKey.new
75
+
76
+ # Create the credential
77
+ credential = Legate::Auth::Credential.new(
78
+ auth_type: :api_key,
79
+ api_key: api_key,
80
+ location: location,
81
+ name: name
82
+ )
83
+
84
+ # Create and return the middleware
85
+ create(scheme: scheme, credential: credential, **options)
86
+ end
87
+
88
+ # Create middleware specifically for Bearer token authentication
89
+ # @param token [String] The bearer token to use
90
+ # @param options [Hash] Additional options for the middleware
91
+ # @return [Legate::Auth::ExconMiddleware] The configured middleware
92
+ def create_bearer(token:, **options)
93
+ # Create the scheme
94
+ scheme = Legate::Auth::Schemes::HTTPBearer.new
95
+
96
+ # Create the credential
97
+ credential = Legate::Auth::Credential.new(
98
+ auth_type: :http_bearer,
99
+ bearer_token: token
100
+ )
101
+
102
+ # Create and return the middleware
103
+ create(scheme: scheme, credential: credential, **options)
104
+ end
105
+
106
+ # Create middleware specifically for OAuth2 authentication
107
+ # @param client_id [String] The OAuth client ID
108
+ # @param client_secret [String] The OAuth client secret
109
+ # @param authorization_url [String] The authorization URL for the OAuth provider
110
+ # @param token_url [String] The token URL for the OAuth provider
111
+ # @param scopes [Array<String>, String] The OAuth scopes to request
112
+ # @param options [Hash] Additional options for the middleware
113
+ # @option options [String] :redirect_uri The redirect URI for the OAuth flow
114
+ # @option options [Hash] :additional_params Additional parameters to include in the authorization request
115
+ # @return [Legate::Auth::ExconMiddleware] The configured middleware
116
+ def create_oauth2(client_id:, client_secret:, authorization_url:, token_url:, scopes: nil, **options)
117
+ # Extract OAuth2-specific options
118
+ redirect_uri = options.delete(:redirect_uri)
119
+ additional_params = options.delete(:additional_params) || {}
120
+
121
+ # Create the scheme with additional params
122
+ scheme_options = {
123
+ authorization_url: authorization_url,
124
+ token_url: token_url,
125
+ scopes: scopes
126
+ }
127
+
128
+ # Add redirect_uri if provided
129
+ scheme_options[:redirect_uri] = redirect_uri if redirect_uri
130
+
131
+ # Add any additional parameters
132
+ scheme_options[:additional_params] = additional_params unless additional_params.empty?
133
+
134
+ # Create the scheme
135
+ scheme = Legate::Auth::Schemes::OAuth2.new(**scheme_options)
136
+
137
+ # Create the credential
138
+ credential = Legate::Auth::Credential.new(
139
+ auth_type: :oauth2,
140
+ client_id: client_id,
141
+ client_secret: client_secret
142
+ )
143
+
144
+ # Create and return the middleware
145
+ create(scheme: scheme, credential: credential, **options)
146
+ end
147
+
148
+ # Create middleware specifically for OpenID Connect authentication
149
+ # @param client_id [String] The OAuth client ID
150
+ # @param client_secret [String] The OAuth client secret
151
+ # @param discovery_url [String, nil] The OIDC discovery URL
152
+ # @param authorization_url [String, nil] The authorization URL (if not using discovery)
153
+ # @param token_url [String, nil] The token URL (if not using discovery)
154
+ # @param userinfo_url [String, nil] The userinfo URL (if not using discovery)
155
+ # @param jwks_url [String, nil] The JWKS URL (if not using discovery)
156
+ # @param scopes [Array<String>, String] The OAuth scopes to request
157
+ # @param options [Hash] Additional options for the middleware
158
+ # @option options [String] :redirect_uri The redirect URI for the OIDC flow
159
+ # @option options [Hash] :additional_params Additional parameters to include in the authorization request
160
+ # @option options [Boolean] :verify_id_token Whether to verify the ID token
161
+ # @return [Legate::Auth::ExconMiddleware] The configured middleware
162
+ def create_oidc(client_id:, client_secret:, discovery_url: nil, authorization_url: nil,
163
+ token_url: nil, userinfo_url: nil, jwks_url: nil, scopes: nil, **options)
164
+ # Extract OIDC-specific options
165
+ redirect_uri = options.delete(:redirect_uri)
166
+ additional_params = options.delete(:additional_params) || {}
167
+ verify_id_token = options.key?(:verify_id_token) ? options.delete(:verify_id_token) : true
168
+
169
+ # Determine how to initialize the scheme
170
+ scheme_options = if discovery_url
171
+ {
172
+ discovery_url: discovery_url,
173
+ scopes: scopes,
174
+ verify_id_token: verify_id_token
175
+ }
176
+ else
177
+ {
178
+ authorization_url: authorization_url,
179
+ token_url: token_url,
180
+ userinfo_url: userinfo_url,
181
+ jwks_url: jwks_url,
182
+ scopes: scopes,
183
+ verify_id_token: verify_id_token
184
+ }
185
+ end
186
+
187
+ # Add redirect_uri if provided
188
+ scheme_options[:redirect_uri] = redirect_uri if redirect_uri
189
+
190
+ # Add any additional parameters
191
+ scheme_options[:additional_params] = additional_params unless additional_params.empty?
192
+
193
+ # Create the scheme
194
+ scheme = Legate::Auth::Schemes::OIDC.new(**scheme_options)
195
+
196
+ # Create the credential
197
+ credential = Legate::Auth::Credential.new(
198
+ auth_type: :oidc,
199
+ client_id: client_id,
200
+ client_secret: client_secret
201
+ )
202
+
203
+ # Create and return the middleware
204
+ create(scheme: scheme, credential: credential, **options)
205
+ end
206
+
207
+ # Create middleware specifically for Service Account authentication
208
+ # @param service_account_key [String, Hash] The service account key as JSON string or Hash
209
+ # @param token_url [String, nil] The token URL for the service account
210
+ # @param scopes [Array<String>, String, nil] The scopes to request
211
+ # @param audience [String, nil] The audience for the token
212
+ # @param options [Hash] Additional options for the middleware
213
+ # @option options [Integer] :token_lifetime Time in seconds for token expiration (default: 3600)
214
+ # @return [Legate::Auth::ExconMiddleware] The configured middleware
215
+ def create_service_account(service_account_key:, token_url: nil, scopes: nil, audience: nil, **options)
216
+ # Extract service account specific options
217
+ token_lifetime = options.delete(:token_lifetime) || 3600
218
+
219
+ # Parse the key if it's a string
220
+ key_data = if service_account_key.is_a?(String)
221
+ begin
222
+ JSON.parse(service_account_key)
223
+ rescue JSON::ParserError
224
+ raise ArgumentError, 'Invalid service account key: not valid JSON'
225
+ end
226
+ else
227
+ service_account_key
228
+ end
229
+
230
+ # Use token_url from the key if not provided
231
+ token_url ||= key_data['token_uri']
232
+
233
+ # Create the scheme
234
+ scheme = Legate::Auth::Schemes::ServiceAccount.new(
235
+ token_url: token_url,
236
+ audience: audience,
237
+ scopes: scopes,
238
+ token_lifetime: token_lifetime
239
+ )
240
+
241
+ # Create the credential
242
+ credential = Legate::Auth::Credential.new(
243
+ auth_type: :service_account,
244
+ service_account_key: service_account_key.is_a?(String) ? service_account_key : service_account_key.to_json
245
+ )
246
+
247
+ # Create and return the middleware
248
+ create(scheme: scheme, credential: credential, **options)
249
+ end
250
+
251
+ # Create middleware specifically for Basic authentication
252
+ # @param username [String] The username for Basic Auth
253
+ # @param password [String] The password for Basic Auth
254
+ # @param options [Hash] Additional options for the middleware
255
+ # @return [Legate::Auth::ExconMiddleware] The configured middleware
256
+ def create_basic_auth(username:, password:, **options)
257
+ # Basic auth is handled by the HTTPBearer scheme with a different type
258
+ scheme = Legate::Auth::Schemes::HTTPBearer.new(auth_type: :basic)
259
+
260
+ # Create the credential
261
+ credential = Legate::Auth::Credential.new(
262
+ auth_type: :basic,
263
+ username: username,
264
+ password: password
265
+ )
266
+
267
+ # Create and return the middleware
268
+ create(scheme: scheme, credential: credential, **options)
269
+ end
270
+
271
+ # Create middleware for a pre-configured authentication provider
272
+ # @param provider_id [String] The ID of the pre-configured provider
273
+ # @param options [Hash] Additional options for the middleware
274
+ # @return [Legate::Auth::ExconMiddleware] The configured middleware
275
+ def create_from_provider(provider_id, **options)
276
+ # Retrieve the stored credential from Legate::Auth
277
+ exchanged_credential = Legate::Auth.get_exchanged_credential(provider_id)
278
+ raise ArgumentError, "No credential found for provider ID: #{provider_id}" unless exchanged_credential
279
+
280
+ # Get the scheme
281
+ scheme = Legate::Auth.get_scheme_for_provider(provider_id)
282
+ raise ArgumentError, "No scheme found for provider ID: #{provider_id}" unless scheme
283
+
284
+ # Create and return the middleware
285
+ create(scheme: scheme, credential: exchanged_credential, **options)
286
+ end
287
+ end
288
+ end
289
+ end
290
+ end