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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +345 -0
- data/bin/legate +13 -0
- data/examples/00_quickstart.rb +51 -0
- data/examples/01_simple_agent.rb +105 -0
- data/examples/02_multi_tool_agent.rb +140 -0
- data/examples/03_custom_tool.rb +93 -0
- data/examples/04_agent_instructions.rb +84 -0
- data/examples/05_state_and_sessions.rb +91 -0
- data/examples/06_callbacks.rb +186 -0
- data/examples/07_async_jobs.rb +112 -0
- data/examples/08_loop_agent.rb +197 -0
- data/examples/09_sequential_workflow.rb +40 -0
- data/examples/10_parallel_workflow.rb +34 -0
- data/examples/11_agent_delegation.rb +24 -0
- data/examples/12_http_client_tool.rb +156 -0
- data/examples/13_authentication.rb +220 -0
- data/examples/14_mcp_client.rb +154 -0
- data/examples/15_mcp_server.rb +79 -0
- data/examples/16_webhooks.rb +91 -0
- data/examples/README_sequential_agents.md +164 -0
- data/examples/advanced/auth/cookie_auth_tool.rb +146 -0
- data/examples/advanced/auth/custom_auth_flows_example.rb +626 -0
- data/examples/advanced/auth/excon_middleware.rb +317 -0
- data/examples/advanced/auth/excon_middleware_auth.rb +399 -0
- data/examples/advanced/auth/fiber_auth_example.rb +281 -0
- data/examples/advanced/auth/fiber_oidc_example.rb +403 -0
- data/examples/advanced/auth/httpbin_bearer_tool.rb +159 -0
- data/examples/advanced/auth/oauth2_auth.rb +419 -0
- data/examples/advanced/auth/oidc_auth.rb +514 -0
- data/examples/advanced/auth/openweather_api.rb +251 -0
- data/examples/advanced/auth/openweather_tool.rb +153 -0
- data/examples/advanced/auth/query_param_middleware_test.rb +138 -0
- data/examples/advanced/auth/service_account.rb +135 -0
- data/examples/advanced/auth/test_with_httpbin.rb +202 -0
- data/examples/advanced/auth/token_lifecycle_example.rb +428 -0
- data/examples/advanced/callback_monitoring.rb +679 -0
- data/examples/advanced/mas/fixed_delegation_example.rb +191 -0
- data/examples/advanced/mas/loop_workflow.rb +28 -0
- data/examples/advanced/mas/mock_planner.rb +77 -0
- data/examples/advanced/mas/proper_delegation_example.rb +276 -0
- data/examples/advanced/mcp/legate_mcp_server_resource_example.rb +182 -0
- data/examples/advanced/mcp/mcp_resource_server_example.rb +309 -0
- data/examples/advanced/mcp/mcp_server_async.rb +76 -0
- data/examples/advanced/mcp/mcp_server_async_tools.rb +122 -0
- data/examples/advanced/mcp/mcp_server_legate_agent.rb +95 -0
- data/examples/advanced/mcp/mcp_server_rack.rb +89 -0
- data/examples/advanced/random_calculator.rb +104 -0
- data/examples/advanced/sleep_agent.rb +153 -0
- data/examples/advanced/webhooks/webhook_e2e_runner.rb +110 -0
- data/examples/advanced/webhooks/webhook_receiver_agent.rb +58 -0
- data/examples/advanced/workflows/task_refinement_loop_agent.rb +278 -0
- data/examples/advanced/workflows/travel_planner_auto_sequential.rb +444 -0
- data/examples/advanced/workflows/travel_planner_parallel.rb +656 -0
- data/examples/advanced/workflows/travel_planner_sequential.rb +512 -0
- data/examples/tools/oauth2_example.rb +136 -0
- data/examples/tools/sleepy_tool.rb +42 -0
- data/lib/legate/activity_log.rb +71 -0
- data/lib/legate/agent.rb +959 -0
- data/lib/legate/agent_code_generator.rb +185 -0
- data/lib/legate/agent_definition.rb +812 -0
- data/lib/legate/agentic/decision.rb +49 -0
- data/lib/legate/agentic/loop.rb +134 -0
- data/lib/legate/agentic.rb +5 -0
- data/lib/legate/agents/loop_agent.rb +248 -0
- data/lib/legate/agents/parallel_agent.rb +163 -0
- data/lib/legate/agents/sequential_agent.rb +190 -0
- data/lib/legate/agents.rb +14 -0
- data/lib/legate/auth/config.rb +148 -0
- data/lib/legate/auth/coordinator.rb +218 -0
- data/lib/legate/auth/coordinators/oauth2_coordinator.rb +99 -0
- data/lib/legate/auth/coordinators/oidc_coordinator.rb +68 -0
- data/lib/legate/auth/coordinators/service_account_coordinator.rb +122 -0
- data/lib/legate/auth/credential.rb +157 -0
- data/lib/legate/auth/encryption.rb +108 -0
- data/lib/legate/auth/error.rb +94 -0
- data/lib/legate/auth/exchanged_credential.rb +180 -0
- data/lib/legate/auth/excon_middleware.rb +285 -0
- data/lib/legate/auth/http_client_utils.rb +364 -0
- data/lib/legate/auth/manager.rb +531 -0
- data/lib/legate/auth/manager_store.rb +394 -0
- data/lib/legate/auth/middleware_factory.rb +290 -0
- data/lib/legate/auth/runner.rb +279 -0
- data/lib/legate/auth/scheme.rb +125 -0
- data/lib/legate/auth/schemes/api_key.rb +212 -0
- data/lib/legate/auth/schemes/google_service_account.rb +108 -0
- data/lib/legate/auth/schemes/http_bearer.rb +98 -0
- data/lib/legate/auth/schemes/oauth2.rb +396 -0
- data/lib/legate/auth/schemes/openid_connect.rb +346 -0
- data/lib/legate/auth/schemes/service_account.rb +388 -0
- data/lib/legate/auth/schemes.rb +40 -0
- data/lib/legate/auth/token_manager.rb +362 -0
- data/lib/legate/auth/token_store.rb +86 -0
- data/lib/legate/auth/tool_context_extension.rb +97 -0
- data/lib/legate/auth/tool_integration.rb +188 -0
- data/lib/legate/auth/url_guard.rb +81 -0
- data/lib/legate/auth.rb +453 -0
- data/lib/legate/callbacks/callback_context.rb +71 -0
- data/lib/legate/cli/agent_commands.rb +950 -0
- data/lib/legate/cli/auth_commands.rb +520 -0
- data/lib/legate/cli/base_command.rb +24 -0
- data/lib/legate/cli/deployment_commands.rb +934 -0
- data/lib/legate/cli/output_helper.rb +108 -0
- data/lib/legate/cli/session_commands.rb +138 -0
- data/lib/legate/cli/skaffold_commands.rb +223 -0
- data/lib/legate/cli/tool_commands.rb +261 -0
- data/lib/legate/cli/web_commands.rb +182 -0
- data/lib/legate/cli.rb +40 -0
- data/lib/legate/configuration/webhooks.rb +113 -0
- data/lib/legate/configuration.rb +39 -0
- data/lib/legate/definition_store.rb +23 -0
- data/lib/legate/errors.rb +118 -0
- data/lib/legate/event.rb +161 -0
- data/lib/legate/gemini_ai_beta_patch.rb +39 -0
- data/lib/legate/generators/agent_generator.rb +412 -0
- data/lib/legate/generators/code_validator.rb +48 -0
- data/lib/legate/generators/legate/install_generator.rb +35 -0
- data/lib/legate/generators/legate/templates/create_legate_tables.rb.tt +36 -0
- data/lib/legate/generators/legate/templates/initializer.rb +18 -0
- data/lib/legate/generators/runtime_tool_loader.rb +76 -0
- data/lib/legate/generators/tool_generator.rb +408 -0
- data/lib/legate/generators.rb +11 -0
- data/lib/legate/global_definition_registry.rb +506 -0
- data/lib/legate/global_tool_manager.rb +135 -0
- data/lib/legate/llm/adapter.rb +69 -0
- data/lib/legate/llm/gemini.rb +172 -0
- data/lib/legate/llm/ollama.rb +80 -0
- data/lib/legate/llm.rb +34 -0
- data/lib/legate/mcp/client.rb +320 -0
- data/lib/legate/mcp/connection/sse.rb +292 -0
- data/lib/legate/mcp/connection/stdio.rb +273 -0
- data/lib/legate/mcp/connection_manager.rb +103 -0
- data/lib/legate/mcp/server/legate_agent_adapter.rb +170 -0
- data/lib/legate/mcp/server/legate_direct_agent_adapter.rb +140 -0
- data/lib/legate/mcp/server/legate_tool_adapter.rb +119 -0
- data/lib/legate/mcp/tool_wrapper.rb +138 -0
- data/lib/legate/mcp/util/schema_converter.rb +134 -0
- data/lib/legate/mcp.rb +23 -0
- data/lib/legate/plan_executor.rb +375 -0
- data/lib/legate/planner.rb +839 -0
- data/lib/legate/rails/railtie.rb +43 -0
- data/lib/legate/rails.rb +9 -0
- data/lib/legate/redaction.rb +32 -0
- data/lib/legate/session.rb +299 -0
- data/lib/legate/session_service/active_record.rb +300 -0
- data/lib/legate/session_service/base.rb +68 -0
- data/lib/legate/session_service/event_broadcast.rb +74 -0
- data/lib/legate/session_service/in_memory.rb +188 -0
- data/lib/legate/tool/metadata_dsl.rb +122 -0
- data/lib/legate/tool.rb +276 -0
- data/lib/legate/tool_code_generator.rb +103 -0
- data/lib/legate/tool_context.rb +350 -0
- data/lib/legate/tool_loader.rb +39 -0
- data/lib/legate/tool_registry.rb +73 -0
- data/lib/legate/tool_result.rb +61 -0
- data/lib/legate/tools/agent_tool.rb +187 -0
- data/lib/legate/tools/base/http_client.rb +319 -0
- data/lib/legate/tools/base/safe_url.rb +56 -0
- data/lib/legate/tools/base_async_job_tool.rb +91 -0
- data/lib/legate/tools/calculator.rb +89 -0
- data/lib/legate/tools/cat_facts.rb +81 -0
- data/lib/legate/tools/check_job_status_tool.rb +48 -0
- data/lib/legate/tools/current_time_tool.rb +64 -0
- data/lib/legate/tools/echo.rb +43 -0
- data/lib/legate/tools/http_request_tool.rb +105 -0
- data/lib/legate/tools/random_number_tool.rb +64 -0
- data/lib/legate/tools/read_webpage_tool.rb +92 -0
- data/lib/legate/tools/sleepy_tool.rb +74 -0
- data/lib/legate/tools/webhook_tool.rb +146 -0
- data/lib/legate/version.rb +5 -0
- data/lib/legate/web/app.rb +984 -0
- data/lib/legate/web/public/css/main.css +4980 -0
- data/lib/legate/web/public/images/favicon-256.png +0 -0
- data/lib/legate/web/public/images/favicon-32.png +0 -0
- data/lib/legate/web/public/images/legate-logo-dark.png +0 -0
- data/lib/legate/web/public/images/legate-logo-light.png +0 -0
- data/lib/legate/web/public/js/legate.js +616 -0
- data/lib/legate/web/public/styles/main.scss +4402 -0
- data/lib/legate/web/routes/agent_authentication_routes.rb +530 -0
- data/lib/legate/web/routes/agent_definition_routes.rb +803 -0
- data/lib/legate/web/routes/agent_generator_routes.rb +80 -0
- data/lib/legate/web/routes/agent_interaction_routes.rb +734 -0
- data/lib/legate/web/routes/agent_runtime_routes.rb +323 -0
- data/lib/legate/web/routes/api_routes.rb +56 -0
- data/lib/legate/web/routes/authentication_routes.rb +1541 -0
- data/lib/legate/web/routes/core_routes.rb +111 -0
- data/lib/legate/web/routes/documentation_routes.rb +220 -0
- data/lib/legate/web/routes/tool_generator_routes.rb +81 -0
- data/lib/legate/web/routes/tools_ui_routes.rb +207 -0
- data/lib/legate/web/sass_compiler.rb +73 -0
- data/lib/legate/web/views/_active_session_info.slim +25 -0
- data/lib/legate/web/views/_activity_list.slim +55 -0
- data/lib/legate/web/views/_agent_card.slim +56 -0
- data/lib/legate/web/views/_agent_generator_modal.slim +382 -0
- data/lib/legate/web/views/_agent_status_controls.slim +71 -0
- data/lib/legate/web/views/_agent_tool_table.slim +74 -0
- data/lib/legate/web/views/_chat_message.slim +95 -0
- data/lib/legate/web/views/_display_agent_configuration.slim +26 -0
- data/lib/legate/web/views/_display_agent_description.slim +11 -0
- data/lib/legate/web/views/_display_agent_fallback.slim +15 -0
- data/lib/legate/web/views/_display_agent_hierarchy.slim +93 -0
- data/lib/legate/web/views/_display_agent_instruction.slim +17 -0
- data/lib/legate/web/views/_display_agent_mcp.slim +13 -0
- data/lib/legate/web/views/_display_agent_model.slim +17 -0
- data/lib/legate/web/views/_display_agent_name.slim +42 -0
- data/lib/legate/web/views/_display_agent_output_key.slim +26 -0
- data/lib/legate/web/views/_display_agent_type.slim +65 -0
- data/lib/legate/web/views/_edit_agent_configuration.slim +74 -0
- data/lib/legate/web/views/_edit_agent_description.slim +16 -0
- data/lib/legate/web/views/_edit_agent_fallback.slim +25 -0
- data/lib/legate/web/views/_edit_agent_hierarchy.slim +98 -0
- data/lib/legate/web/views/_edit_agent_instruction.slim +49 -0
- data/lib/legate/web/views/_edit_agent_mcp.slim +33 -0
- data/lib/legate/web/views/_edit_agent_model.slim +23 -0
- data/lib/legate/web/views/_edit_agent_output_key.slim +36 -0
- data/lib/legate/web/views/_edit_agent_tools.slim +40 -0
- data/lib/legate/web/views/_edit_agent_type.slim +67 -0
- data/lib/legate/web/views/_session_error.slim +4 -0
- data/lib/legate/web/views/_skeleton.slim +69 -0
- data/lib/legate/web/views/_tool_card.slim +9 -0
- data/lib/legate/web/views/_tool_generator_modal.slim +311 -0
- data/lib/legate/web/views/agent.slim +436 -0
- data/lib/legate/web/views/agent_auth.slim +562 -0
- data/lib/legate/web/views/agents.slim +369 -0
- data/lib/legate/web/views/auth.slim +112 -0
- data/lib/legate/web/views/auth_credential_detail.slim +327 -0
- data/lib/legate/web/views/auth_credentials.slim +261 -0
- data/lib/legate/web/views/auth_debug.slim +94 -0
- data/lib/legate/web/views/auth_mapping_detail.slim +151 -0
- data/lib/legate/web/views/auth_mapping_new.slim +123 -0
- data/lib/legate/web/views/auth_mappings.slim +120 -0
- data/lib/legate/web/views/auth_scheme_detail.slim +274 -0
- data/lib/legate/web/views/auth_schemes.slim +259 -0
- data/lib/legate/web/views/auth_test.slim +418 -0
- data/lib/legate/web/views/chat.slim +192 -0
- data/lib/legate/web/views/docs_index.slim +105 -0
- data/lib/legate/web/views/docs_show.slim +105 -0
- data/lib/legate/web/views/error_404.slim +5 -0
- data/lib/legate/web/views/index.slim +148 -0
- data/lib/legate/web/views/layout.slim +144 -0
- data/lib/legate/web/views/tool_detail.slim +87 -0
- data/lib/legate/web/views/tools.slim +50 -0
- data/lib/legate/web/webhook_listener.rb +367 -0
- data/lib/legate/web.rb +9 -0
- data/lib/legate.rb +220 -0
- data/public/docs/advanced/callbacks.md +828 -0
- data/public/docs/advanced/mcp_schema_conversion.md +59 -0
- data/public/docs/authentication/api_reference/config.md +210 -0
- data/public/docs/authentication/api_reference/credential.md +246 -0
- data/public/docs/authentication/api_reference/encryption.md +218 -0
- data/public/docs/authentication/api_reference/exchanged_credential.md +271 -0
- data/public/docs/authentication/api_reference/excon_middleware.md +175 -0
- data/public/docs/authentication/api_reference/index.md +30 -0
- data/public/docs/authentication/api_reference/scheme.md +250 -0
- data/public/docs/authentication/api_reference/schemes/api_key.md +175 -0
- data/public/docs/authentication/api_reference/schemes/google_service_account.md +221 -0
- data/public/docs/authentication/api_reference/schemes/http_bearer.md +169 -0
- data/public/docs/authentication/api_reference/schemes/oauth2.md +343 -0
- data/public/docs/authentication/api_reference/schemes/oidc.md +73 -0
- data/public/docs/authentication/api_reference/schemes/openid_connect.md +311 -0
- data/public/docs/authentication/api_reference/schemes/service_account.md +287 -0
- data/public/docs/authentication/api_reference/token_manager.md +221 -0
- data/public/docs/authentication/api_reference/token_store.md +146 -0
- data/public/docs/authentication/api_reference/tool_context_extension.md +166 -0
- data/public/docs/authentication/guides/api_key.md +190 -0
- data/public/docs/authentication/guides/bearer.md +172 -0
- data/public/docs/authentication/guides/configuration.md +255 -0
- data/public/docs/authentication/guides/custom_flow.md +523 -0
- data/public/docs/authentication/guides/index.md +24 -0
- data/public/docs/authentication/guides/migration.md +435 -0
- data/public/docs/authentication/guides/oauth2.md +252 -0
- data/public/docs/authentication/guides/oidc.md +241 -0
- data/public/docs/authentication/guides/overview.md +155 -0
- data/public/docs/authentication/guides/secure_storage.md +301 -0
- data/public/docs/authentication/guides/service_account.md +228 -0
- data/public/docs/authentication/guides/token_lifecycle.md +295 -0
- data/public/docs/authentication/guides/web_ui_integration.md +504 -0
- data/public/docs/authentication/index.md +58 -0
- data/public/docs/authentication/troubleshooting/credential_storage.md +550 -0
- data/public/docs/authentication/troubleshooting/environment_variables.md +540 -0
- data/public/docs/authentication/troubleshooting/index.md +11 -0
- data/public/docs/authentication/troubleshooting/oauth2_issues.md +220 -0
- data/public/docs/authentication/troubleshooting/oidc_issues.md +412 -0
- data/public/docs/authentication/troubleshooting/token_refresh.md +338 -0
- data/public/docs/cli/legate_cli_usage.md +363 -0
- data/public/docs/core_concepts/legate_agent_lifecycle.md +124 -0
- data/public/docs/core_concepts/legate_architecture_overview.md +110 -0
- data/public/docs/core_concepts/legate_configuration.md +116 -0
- data/public/docs/core_concepts/legate_definition_store.md +102 -0
- data/public/docs/core_concepts/legate_planner.md +94 -0
- data/public/docs/core_concepts/legate_session_service.md +104 -0
- data/public/docs/error_handling/legate_error_handling.md +122 -0
- data/public/docs/examples.md +199 -0
- data/public/docs/getting_started.md +111 -0
- data/public/docs/guides/agentic_agents.md +137 -0
- data/public/docs/guides/ai_code_generators.md +437 -0
- data/public/docs/guides/auto_loading.md +326 -0
- data/public/docs/guides/configuring_agent_webhooks.md +219 -0
- data/public/docs/guides/http_client_usage.md +264 -0
- data/public/docs/guides/llm_providers.md +137 -0
- data/public/docs/guides/mcp_client_integration.md +232 -0
- data/public/docs/guides/mcp_server_exposure.md +206 -0
- data/public/docs/guides/rails_integration.md +128 -0
- data/public/docs/guides/sending_outbound_webhooks.md +227 -0
- data/public/docs/guides/streaming.md +112 -0
- data/public/docs/guides/webhooks.md +288 -0
- data/public/docs/introduction.md +51 -0
- data/public/docs/multi_agent_systems/advanced_features.md +57 -0
- data/public/docs/multi_agent_systems/agent_delegation.md +190 -0
- data/public/docs/multi_agent_systems/agent_hierarchy.md +49 -0
- data/public/docs/multi_agent_systems/state_management.md +47 -0
- data/public/docs/multi_agent_systems/workflow_agents.md +72 -0
- data/public/docs/tools/legate_built_in_tools.md +332 -0
- data/public/docs/tools/legate_tools_and_registry.md +263 -0
- data/public/docs/web_ui/legate_web_ui.md +137 -0
- metadata +823 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# File: lib/legate/auth/exchanged_credential.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'time'
|
|
5
|
+
require 'jwt'
|
|
6
|
+
|
|
7
|
+
module Legate
|
|
8
|
+
module Auth
|
|
9
|
+
# Represents credentials that have been exchanged for tokens.
|
|
10
|
+
# Stores tokens obtained from authentication providers, along with
|
|
11
|
+
# metadata such as expiration times and refresh tokens.
|
|
12
|
+
class ExchangedCredential
|
|
13
|
+
# @return [Symbol] The type of authentication
|
|
14
|
+
attr_reader :auth_type
|
|
15
|
+
|
|
16
|
+
# @return [String] The access token
|
|
17
|
+
attr_reader :access_token
|
|
18
|
+
|
|
19
|
+
# @return [String, nil] The refresh token, if available
|
|
20
|
+
attr_reader :refresh_token
|
|
21
|
+
|
|
22
|
+
# @return [String, nil] The token type (e.g., "Bearer")
|
|
23
|
+
attr_reader :token_type
|
|
24
|
+
|
|
25
|
+
# @return [Time, nil] The expiration time
|
|
26
|
+
attr_reader :expires_at
|
|
27
|
+
|
|
28
|
+
# @return [String, nil] ID token for OIDC
|
|
29
|
+
attr_reader :id_token
|
|
30
|
+
|
|
31
|
+
# @return [String, nil] The provider ID for this credential
|
|
32
|
+
attr_accessor :provider_id
|
|
33
|
+
|
|
34
|
+
# @return [Hash] Additional attributes specific to the auth type
|
|
35
|
+
attr_reader :attributes
|
|
36
|
+
|
|
37
|
+
# Initialize a new ExchangedCredential
|
|
38
|
+
# @param auth_type [Symbol] The type of authentication
|
|
39
|
+
# @param access_token [String] The access token
|
|
40
|
+
# @param refresh_token [String, nil] The refresh token
|
|
41
|
+
# @param token_type [String, nil] The token type
|
|
42
|
+
# @param expires_in [Integer, nil] Seconds until the token expires
|
|
43
|
+
# @param id_token [String, nil] ID token for OIDC
|
|
44
|
+
# @param provider_id [String, nil] The provider ID for this credential
|
|
45
|
+
# @param attributes [Hash] Additional attributes
|
|
46
|
+
def initialize(auth_type:, access_token:, refresh_token: nil, token_type: 'Bearer',
|
|
47
|
+
expires_in: nil, id_token: nil, provider_id: nil, **attributes)
|
|
48
|
+
@auth_type = auth_type.to_sym
|
|
49
|
+
@access_token = access_token
|
|
50
|
+
@refresh_token = refresh_token
|
|
51
|
+
@token_type = token_type || 'Bearer'
|
|
52
|
+
@id_token = id_token
|
|
53
|
+
@provider_id = provider_id
|
|
54
|
+
@attributes = attributes || {}
|
|
55
|
+
|
|
56
|
+
# Calculate expiration time if expires_in is provided
|
|
57
|
+
@expires_at = if expires_in && expires_in.to_i > 0
|
|
58
|
+
Time.now + expires_in.to_i
|
|
59
|
+
elsif attributes[:expires_at]
|
|
60
|
+
Time.parse(attributes[:expires_at].to_s)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Check if the token is expired
|
|
65
|
+
# @param buffer_seconds [Integer] Buffer time in seconds to consider token as expired
|
|
66
|
+
# @return [Boolean] True if the token is expired, false otherwise
|
|
67
|
+
def expired?(buffer_seconds = 30)
|
|
68
|
+
return false unless @expires_at
|
|
69
|
+
|
|
70
|
+
@expires_at - buffer_seconds <= Time.now
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Check if the credential can be refreshed
|
|
74
|
+
# @return [Boolean] True if a refresh token is available
|
|
75
|
+
def refreshable?
|
|
76
|
+
!@refresh_token.nil? && !@refresh_token.empty?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Returns the decoded claims from the ID token
|
|
80
|
+
# @return [Hash] The parsed ID token claims, or an empty hash if no ID token
|
|
81
|
+
def id_token_claims
|
|
82
|
+
return {} unless @id_token
|
|
83
|
+
|
|
84
|
+
begin
|
|
85
|
+
JWT.decode(@id_token, nil, false)[0]
|
|
86
|
+
rescue JWT::DecodeError => e
|
|
87
|
+
{}
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Convert to a hash for serialization
|
|
92
|
+
# @return [Hash] A hash representation of the credential
|
|
93
|
+
def to_h
|
|
94
|
+
{
|
|
95
|
+
auth_type: @auth_type,
|
|
96
|
+
access_token: @access_token,
|
|
97
|
+
refresh_token: @refresh_token,
|
|
98
|
+
token_type: @token_type,
|
|
99
|
+
expires_at: @expires_at&.iso8601,
|
|
100
|
+
id_token: @id_token,
|
|
101
|
+
provider_id: @provider_id
|
|
102
|
+
}.merge(@attributes).compact
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Create an ExchangedCredential from a hash
|
|
106
|
+
# @param hash [Hash] A hash representation of the credential
|
|
107
|
+
# @return [Legate::Auth::ExchangedCredential] A new ExchangedCredential
|
|
108
|
+
def self.from_h(hash)
|
|
109
|
+
attrs = hash.dup
|
|
110
|
+
auth_type = attrs.delete(:auth_type) || attrs.delete('auth_type')
|
|
111
|
+
access_token = attrs.delete(:access_token) || attrs.delete('access_token')
|
|
112
|
+
refresh_token = attrs.delete(:refresh_token) || attrs.delete('refresh_token')
|
|
113
|
+
token_type = attrs.delete(:token_type) || attrs.delete('token_type')
|
|
114
|
+
expires_at = attrs.delete(:expires_at) || attrs.delete('expires_at')
|
|
115
|
+
id_token = attrs.delete(:id_token) || attrs.delete('id_token')
|
|
116
|
+
provider_id = attrs.delete(:provider_id) || attrs.delete('provider_id')
|
|
117
|
+
|
|
118
|
+
# Convert string keys to symbols
|
|
119
|
+
attributes = {}
|
|
120
|
+
attrs.each do |key, value|
|
|
121
|
+
attributes[key.to_sym] = value
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Set expires_at as an attribute so it gets passed to the initializer
|
|
125
|
+
attributes[:expires_at] = expires_at if expires_at
|
|
126
|
+
|
|
127
|
+
new(
|
|
128
|
+
auth_type: auth_type,
|
|
129
|
+
access_token: access_token,
|
|
130
|
+
refresh_token: refresh_token,
|
|
131
|
+
token_type: token_type,
|
|
132
|
+
id_token: id_token,
|
|
133
|
+
provider_id: provider_id,
|
|
134
|
+
**attributes
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Get an attribute value
|
|
139
|
+
# @param name [Symbol, String] The attribute name
|
|
140
|
+
# @return [Object, nil] The attribute value, or nil if not present
|
|
141
|
+
def [](name)
|
|
142
|
+
case name.to_sym
|
|
143
|
+
when :access_token
|
|
144
|
+
@access_token
|
|
145
|
+
when :refresh_token
|
|
146
|
+
@refresh_token
|
|
147
|
+
when :token_type
|
|
148
|
+
@token_type
|
|
149
|
+
when :expires_at
|
|
150
|
+
@expires_at
|
|
151
|
+
when :id_token
|
|
152
|
+
@id_token
|
|
153
|
+
when :auth_type
|
|
154
|
+
@auth_type
|
|
155
|
+
when :provider_id
|
|
156
|
+
@provider_id
|
|
157
|
+
else
|
|
158
|
+
@attributes[name.to_sym]
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Return a new ExchangedCredential with updated values
|
|
163
|
+
# @param attrs [Hash] The attributes to update
|
|
164
|
+
# @return [Legate::Auth::ExchangedCredential] A new ExchangedCredential with updated values
|
|
165
|
+
def with(attrs)
|
|
166
|
+
self.class.new(
|
|
167
|
+
auth_type: attrs[:auth_type] || @auth_type,
|
|
168
|
+
access_token: attrs[:access_token] || @access_token,
|
|
169
|
+
refresh_token: attrs[:refresh_token] || @refresh_token,
|
|
170
|
+
token_type: attrs[:token_type] || @token_type,
|
|
171
|
+
id_token: attrs[:id_token] || @id_token,
|
|
172
|
+
provider_id: attrs[:provider_id] || @provider_id,
|
|
173
|
+
**@attributes.merge(attrs.reject { |k, _|
|
|
174
|
+
%i[auth_type access_token refresh_token token_type id_token provider_id].include?(k)
|
|
175
|
+
})
|
|
176
|
+
)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# File: lib/legate/auth/excon_middleware.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'excon'
|
|
5
|
+
require_relative 'tool_integration'
|
|
6
|
+
|
|
7
|
+
module Legate
|
|
8
|
+
module Auth
|
|
9
|
+
# Excon middleware for automatically handling authentication
|
|
10
|
+
# This middleware can be inserted into the Excon middleware stack
|
|
11
|
+
# to automatically apply authentication to requests and handle
|
|
12
|
+
# authentication errors.
|
|
13
|
+
class ExconMiddleware < Excon::Middleware::Base
|
|
14
|
+
# Class-level new method for both factory creation and Excon middleware stack
|
|
15
|
+
def self.new(*args)
|
|
16
|
+
if args.length == 1 && args[0].is_a?(Array)
|
|
17
|
+
# Called by Excon's middleware stack with just the stack
|
|
18
|
+
super(args[0])
|
|
19
|
+
else
|
|
20
|
+
# Called by our factory with options
|
|
21
|
+
stack, options = args
|
|
22
|
+
super(stack, options || {})
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Attributes needed by the shell middleware when accessing the configured instance
|
|
27
|
+
attr_reader :scheme, :credential, :token_store, :token_manager
|
|
28
|
+
attr_reader :auto_retry, :max_retries, :backoff_strategy, :backoff_factor, :retry_non_idempotent, :retry_on
|
|
29
|
+
|
|
30
|
+
# Initialize the middleware
|
|
31
|
+
# @param stack [Array] The middleware stack
|
|
32
|
+
# @param options [Hash] The options for configuring the middleware
|
|
33
|
+
def initialize(stack, options = {})
|
|
34
|
+
super(stack)
|
|
35
|
+
|
|
36
|
+
@scheme = options[:scheme]
|
|
37
|
+
@credential = options[:credential]
|
|
38
|
+
@token_store = options[:token_store]
|
|
39
|
+
@token_manager = options[:token_manager]
|
|
40
|
+
@auto_retry = options.fetch(:auto_retry, true)
|
|
41
|
+
@max_retries = options.fetch(:max_retries, 3)
|
|
42
|
+
@backoff_strategy = options.fetch(:backoff_strategy, :exponential)
|
|
43
|
+
@backoff_factor = options.fetch(:backoff_factor, 1.0)
|
|
44
|
+
@retry_non_idempotent = options.fetch(:retry_non_idempotent, false)
|
|
45
|
+
@retry_on = Array(options.fetch(:retry_on, [])) + [401, 403]
|
|
46
|
+
|
|
47
|
+
if @scheme && @credential
|
|
48
|
+
Legate.logger.debug("ExconMiddleware: Factory-created instance configured: #{@scheme.scheme_type}") if defined?(Legate.logger)
|
|
49
|
+
register_token_lifecycle_callbacks if @token_manager && @token_manager.respond_to?(:register_callback)
|
|
50
|
+
elsif defined?(Legate.logger)
|
|
51
|
+
# This is the shell instance created by Excon
|
|
52
|
+
Legate.logger.debug('ExconMiddleware: Shell instance initialized by Excon.')
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Called for each request in the Excon middleware stack
|
|
57
|
+
# @param datum [Hash] The request/response data
|
|
58
|
+
# @yield [Hash] The updated request/response data
|
|
59
|
+
# @return [Hash] The request/response data
|
|
60
|
+
def request_call(datum)
|
|
61
|
+
# Determine if this is the shell or the configured instance
|
|
62
|
+
# The shell instance will have a non-nil @stack from Excon,
|
|
63
|
+
# and its @scheme will be nil (as Excon doesn't pass those to initialize by default)
|
|
64
|
+
is_shell_instance = @scheme.nil? && @stack
|
|
65
|
+
|
|
66
|
+
if is_shell_instance
|
|
67
|
+
config_instance = datum[:connection].data[:auth_middleware_config]
|
|
68
|
+
unless config_instance
|
|
69
|
+
Legate.logger.warn('ExconMiddleware (shell): No :auth_middleware_config found. Passing through.') if defined?(Legate.logger)
|
|
70
|
+
return @stack.request_call(datum)
|
|
71
|
+
end
|
|
72
|
+
Legate.logger.debug('ExconMiddleware (shell) delegating to configured instance for request logic.') if defined?(Legate.logger)
|
|
73
|
+
# Modify datum using logic from config_instance, then shell calls @stack
|
|
74
|
+
apply_authentication_logic(datum, config_instance)
|
|
75
|
+
result = @stack.request_call(datum)
|
|
76
|
+
result[:request] = datum[:request] if datum[:request]
|
|
77
|
+
result
|
|
78
|
+
else
|
|
79
|
+
# This is the factory-configured instance, being called directly (e.g. by the shell, or in tests)
|
|
80
|
+
# It should not call @stack.request_call itself if its @stack is the factory-provided nil.
|
|
81
|
+
Legate.logger.debug('ExconMiddleware (configured instance) applying auth logic directly.') if defined?(Legate.logger)
|
|
82
|
+
apply_authentication_logic(datum, self) # Apply logic using its own config
|
|
83
|
+
datum
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Called after each response in the Excon middleware stack
|
|
88
|
+
# @param datum [Hash] The request/response data
|
|
89
|
+
# @yield [Hash] The updated request/response data
|
|
90
|
+
# @return [Hash] The request/response data
|
|
91
|
+
def response_call(datum)
|
|
92
|
+
is_shell_instance = @scheme.nil? && @stack
|
|
93
|
+
|
|
94
|
+
if is_shell_instance
|
|
95
|
+
# Shell instance calls down the stack first
|
|
96
|
+
response_datum = @stack.response_call(datum)
|
|
97
|
+
|
|
98
|
+
config_instance = datum[:connection].data[:auth_middleware_config]
|
|
99
|
+
unless config_instance
|
|
100
|
+
Legate.logger.warn('ExconMiddleware (shell): No :auth_middleware_config for response. Passing through.') if defined?(Legate.logger)
|
|
101
|
+
return response_datum
|
|
102
|
+
end
|
|
103
|
+
Legate.logger.debug('ExconMiddleware (shell) delegating to configured instance for response logic.') if defined?(Legate.logger)
|
|
104
|
+
|
|
105
|
+
# Process response and handle retries
|
|
106
|
+
if config_instance.auto_retry && should_retry?(response_datum[:request], response_datum[:response])
|
|
107
|
+
config_instance.token_manager.invalidate_token(config_instance.scheme, config_instance.credential) if config_instance.token_manager && authentication_error?(response_datum[:response])
|
|
108
|
+
# Re-apply authentication with fresh credentials
|
|
109
|
+
apply_authentication_logic(response_datum, config_instance)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
response_datum
|
|
113
|
+
else
|
|
114
|
+
# This is the factory-configured instance, being called by the shell.
|
|
115
|
+
Legate.logger.debug('ExconMiddleware (configured instance) processing response logic directly.') if defined?(Legate.logger)
|
|
116
|
+
|
|
117
|
+
# Process response and handle retries
|
|
118
|
+
if @auto_retry && should_retry?(datum[:request], datum[:response])
|
|
119
|
+
@token_manager.invalidate_token(@scheme, @credential) if @token_manager && authentication_error?(datum[:response])
|
|
120
|
+
# Re-apply authentication with fresh credentials
|
|
121
|
+
apply_authentication_logic(datum, self)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
datum
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def should_retry?(request_datum, response_details)
|
|
129
|
+
return false unless request_datum && response_details
|
|
130
|
+
return false unless @auto_retry
|
|
131
|
+
|
|
132
|
+
status = response_details[:status]
|
|
133
|
+
return false unless status
|
|
134
|
+
|
|
135
|
+
# Check if it's a non-idempotent request
|
|
136
|
+
unless @retry_non_idempotent
|
|
137
|
+
method = request_datum[:method]&.to_s&.upcase
|
|
138
|
+
return false if method && !%w[GET HEAD OPTIONS].include?(method)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Check retry conditions
|
|
142
|
+
return true if @retry_on.include?(status)
|
|
143
|
+
return true if authentication_error?(response_details)
|
|
144
|
+
return true if (500..599).cover?(status)
|
|
145
|
+
return true if response_details[:headers]&.key?('Retry-After')
|
|
146
|
+
|
|
147
|
+
false
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private
|
|
151
|
+
|
|
152
|
+
def register_token_lifecycle_callbacks
|
|
153
|
+
@token_manager.register_callback(:token_refreshed) do |scheme, credential, token|
|
|
154
|
+
Legate.logger.info("Token refreshed for #{scheme.scheme_type}") if defined?(Legate.logger)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
@token_manager.register_callback(:token_invalidated) do |scheme, credential|
|
|
158
|
+
Legate.logger.info("Token invalidated for #{scheme.scheme_type}") if defined?(Legate.logger)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
return unless @token_manager.respond_to?(:register_callback)
|
|
162
|
+
|
|
163
|
+
@token_manager.register_callback(:token_expiring) do |scheme, credential, token, time|
|
|
164
|
+
Legate.logger.info("Token expiring in #{time}s for #{scheme.scheme_type}") if defined?(Legate.logger)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Extracted logic that operates on datum using a config object (which can be self or another instance)
|
|
169
|
+
def apply_authentication_logic(datum, config)
|
|
170
|
+
Legate.logger.debug("Applying auth logic using config: #{config.object_id}, scheme: #{config.scheme&.scheme_type}") if defined?(Legate.logger)
|
|
171
|
+
datum[:request] ||= {}
|
|
172
|
+
request_fields = %i[scheme method path host port query]
|
|
173
|
+
request_fields.each do |field|
|
|
174
|
+
datum[:request][field] ||= datum[field] if datum.key?(field) && datum[field]
|
|
175
|
+
end
|
|
176
|
+
datum[:request][:headers] ||= {}
|
|
177
|
+
|
|
178
|
+
if config.should_authenticate_with_config?(datum[:request], config)
|
|
179
|
+
begin
|
|
180
|
+
cred_to_use = config.token_manager ? (config.token_manager.get_token(config.scheme, config.credential) || config.credential) : config.credential
|
|
181
|
+
|
|
182
|
+
if config.scheme.is_a?(Legate::Auth::Schemes::ApiKey) && cred_to_use && cred_to_use[:location] == 'query'
|
|
183
|
+
api_key_name = cred_to_use[:name]
|
|
184
|
+
api_key_value = cred_to_use[:api_key]
|
|
185
|
+
datum[:query] ||= {}
|
|
186
|
+
if datum[:query].is_a?(String)
|
|
187
|
+
require 'uri'
|
|
188
|
+
current_params = {}
|
|
189
|
+
URI.decode_www_form(datum[:query]).each { |k, v| current_params[k] = v }
|
|
190
|
+
datum[:query] = current_params
|
|
191
|
+
end
|
|
192
|
+
datum[:query][api_key_name] = api_key_value
|
|
193
|
+
datum[:request][:query] = datum[:query]
|
|
194
|
+
datum.delete(:query_string)
|
|
195
|
+
Legate.logger.debug("Added API key to query: #{api_key_name}=REDACTED") if defined?(Legate.logger)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
auth_req = ToolIntegration.apply_authentication(datum[:request], config.scheme, cred_to_use, config.token_store)
|
|
199
|
+
if auth_req
|
|
200
|
+
datum[:request][:headers].merge!(auth_req[:headers] || {})
|
|
201
|
+
if auth_req[:query]
|
|
202
|
+
datum[:query] ||= {}
|
|
203
|
+
if datum[:query].is_a?(Hash) && auth_req[:query].is_a?(Hash)
|
|
204
|
+
datum[:query].merge!(auth_req[:query])
|
|
205
|
+
else
|
|
206
|
+
datum[:query] = auth_req[:query]
|
|
207
|
+
end
|
|
208
|
+
datum[:request][:query] = datum[:query]
|
|
209
|
+
end
|
|
210
|
+
datum[:authenticated] = true
|
|
211
|
+
end
|
|
212
|
+
Legate.logger.debug("Auth applied. Query: #{datum[:query].inspect}") if defined?(Legate.logger)
|
|
213
|
+
rescue StandardError => e
|
|
214
|
+
Legate.logger.error("Failed to apply auth: #{e.message} #{e.backtrace.join("\n")}") if defined?(Legate.logger)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
if datum[:query].is_a?(Hash)
|
|
218
|
+
datum[:query] = datum[:query].transform_keys(&:to_s)
|
|
219
|
+
datum[:request][:query] = datum[:query] if datum[:request]
|
|
220
|
+
end
|
|
221
|
+
Legate.logger.info("[AuthMiddleware] Outgoing query: #{datum[:query].inspect}") if defined?(Legate.logger)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def process_response_logic(response_datum, config)
|
|
225
|
+
if defined?(Legate.logger) && Legate.logger.debug?
|
|
226
|
+
actual_res = response_datum[:response] || response_datum
|
|
227
|
+
Legate.logger.debug("Processing response. Status: #{actual_res[:status] || 'unknown'}")
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
if config.auto_retry && should_retry?(response_datum[:request] || response_datum, response_datum[:response] || response_datum)
|
|
231
|
+
config.token_manager.invalidate_token(config.scheme, config.credential) if config.token_manager && authentication_error?(response_datum[:response] || response_datum)
|
|
232
|
+
Legate.logger.info('Auth retry needed, Idempotent middleware should handle.') if defined?(Legate.logger)
|
|
233
|
+
end
|
|
234
|
+
response_datum
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Methods intended for internal use by the class or subclasses,
|
|
238
|
+
# or when operating on an explicit instance (like the config_instance)
|
|
239
|
+
protected
|
|
240
|
+
|
|
241
|
+
def should_authenticate_with_config?(request_datum, config)
|
|
242
|
+
return false if config.scheme.nil? || config.credential.nil?
|
|
243
|
+
return true if requires_authentication?(request_datum)
|
|
244
|
+
|
|
245
|
+
false
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def requires_authentication?(request)
|
|
249
|
+
return true if request[:method]&.to_s&.upcase != 'GET'
|
|
250
|
+
return false if request[:path]&.start_with?('/public/')
|
|
251
|
+
|
|
252
|
+
true
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def authentication_error?(response)
|
|
256
|
+
return false unless response
|
|
257
|
+
|
|
258
|
+
status = response[:status]
|
|
259
|
+
return true if [401, 403].include?(status)
|
|
260
|
+
return true if response[:body]&.include?('authentication failed')
|
|
261
|
+
|
|
262
|
+
false
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def calculate_backoff_time(retry_count)
|
|
266
|
+
case @backoff_strategy
|
|
267
|
+
when :none then 0.0
|
|
268
|
+
when :linear then retry_count * @backoff_factor
|
|
269
|
+
when :exponential then (2**retry_count) * @backoff_factor
|
|
270
|
+
when :fibonacci
|
|
271
|
+
fib = ->(n) { n <= 1 ? n : fib[n - 1] + fib[n - 2] }
|
|
272
|
+
fib[retry_count + 1] * @backoff_factor # Add 1 to match test expectations
|
|
273
|
+
when :jitter
|
|
274
|
+
# For jitter, we want to ensure the total time is <= 2.0
|
|
275
|
+
# So we'll cap the base at 1.8 and jitter at 0.2
|
|
276
|
+
base = [retry_count * @backoff_factor, 1.8].min
|
|
277
|
+
max_jitter = [0.2, 2.0 - base].min
|
|
278
|
+
base + (rand * max_jitter)
|
|
279
|
+
else
|
|
280
|
+
(2**retry_count) * @backoff_factor
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|