codexLB-up 1.25.0b10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- app/__init__.py +10 -0
- app/admin_cli.py +494 -0
- app/cli.py +339 -0
- app/codex_sessions_retag.py +487 -0
- app/core/__init__.py +0 -0
- app/core/audit/__init__.py +0 -0
- app/core/audit/service.py +216 -0
- app/core/audit/types.py +94 -0
- app/core/auth/__init__.py +236 -0
- app/core/auth/api_key_cache.py +65 -0
- app/core/auth/dashboard_access.py +429 -0
- app/core/auth/dashboard_mode.py +171 -0
- app/core/auth/dashboard_session_ttl.py +74 -0
- app/core/auth/dashboard_users_cache.py +120 -0
- app/core/auth/dependencies.py +710 -0
- app/core/auth/external_identity.py +78 -0
- app/core/auth/guardian.py +374 -0
- app/core/auth/models.py +49 -0
- app/core/auth/providers/__init__.py +129 -0
- app/core/auth/providers/oidc.py +838 -0
- app/core/auth/providers/registry.py +118 -0
- app/core/auth/refresh.py +394 -0
- app/core/auth/step_up.py +117 -0
- app/core/auth/totp.py +81 -0
- app/core/balancer/__init__.py +71 -0
- app/core/balancer/logic.py +1612 -0
- app/core/balancer/rendezvous_hash.py +21 -0
- app/core/balancer/types.py +21 -0
- app/core/bootstrap.py +172 -0
- app/core/cache/invalidation.py +426 -0
- app/core/clients/__init__.py +0 -0
- app/core/clients/codex.py +996 -0
- app/core/clients/codex_version.py +187 -0
- app/core/clients/codex_version_snapshot.py +2 -0
- app/core/clients/files.py +534 -0
- app/core/clients/headers.py +29 -0
- app/core/clients/http.py +511 -0
- app/core/clients/model_fetcher.py +276 -0
- app/core/clients/native_egress.py +1304 -0
- app/core/clients/oauth.py +477 -0
- app/core/clients/proxy.py +6089 -0
- app/core/clients/proxy_websocket.py +1411 -0
- app/core/clients/rate_limit_reset_credits.py +412 -0
- app/core/clients/stream_errors.py +12 -0
- app/core/clients/thread_cache_identity.py +347 -0
- app/core/clients/usage.py +467 -0
- app/core/clock.py +170 -0
- app/core/config/__init__.py +0 -0
- app/core/config/background_jobs.py +73 -0
- app/core/config/context_window_overrides.py +166 -0
- app/core/config/dashboard_overrides.py +145 -0
- app/core/config/inheritable.py +47 -0
- app/core/config/key_fingerprint.py +115 -0
- app/core/config/settings.py +872 -0
- app/core/config/settings_cache.py +100 -0
- app/core/config/spool_retention.py +217 -0
- app/core/config/tiers.py +174 -0
- app/core/conversation_archive.py +568 -0
- app/core/crypto.py +37 -0
- app/core/draining.py +11 -0
- app/core/errors.py +365 -0
- app/core/exceptions.py +155 -0
- app/core/handlers/__init__.py +3 -0
- app/core/handlers/exceptions.py +478 -0
- app/core/http_protocol.py +176 -0
- app/core/http_protocol_httptools.py +204 -0
- app/core/ingress_limits.py +17 -0
- app/core/metadata_store.py +18 -0
- app/core/metrics/__init__.py +0 -0
- app/core/metrics/middleware.py +74 -0
- app/core/metrics/prometheus.py +604 -0
- app/core/middleware/__init__.py +25 -0
- app/core/middleware/api_firewall.py +111 -0
- app/core/middleware/app_version.py +37 -0
- app/core/middleware/dashboard_auth_proxy.py +55 -0
- app/core/middleware/dashboard_csrf.py +177 -0
- app/core/middleware/dashboard_gzip.py +41 -0
- app/core/middleware/dashboard_overrides.py +59 -0
- app/core/middleware/firewall_cache.py +91 -0
- app/core/middleware/inflight.py +126 -0
- app/core/middleware/multipart_content_encoding.py +129 -0
- app/core/middleware/path_rewrite.py +148 -0
- app/core/middleware/request_body_limit.py +196 -0
- app/core/middleware/request_decompression.py +209 -0
- app/core/middleware/request_id.py +72 -0
- app/core/middleware/required_capability_http.py +116 -0
- app/core/middleware/trusted_proxy_headers.py +36 -0
- app/core/multipart.py +237 -0
- app/core/multipart_fields.py +91 -0
- app/core/openai/__init__.py +0 -0
- app/core/openai/chat_requests.py +493 -0
- app/core/openai/chat_responses.py +931 -0
- app/core/openai/contracts.py +46 -0
- app/core/openai/exceptions.py +24 -0
- app/core/openai/host_models.py +14 -0
- app/core/openai/images.py +413 -0
- app/core/openai/message_coercion.py +376 -0
- app/core/openai/model_refresh_scheduler.py +423 -0
- app/core/openai/model_registry.py +997 -0
- app/core/openai/model_registry_store.py +548 -0
- app/core/openai/models.py +172 -0
- app/core/openai/parsing.py +97 -0
- app/core/openai/requests.py +1980 -0
- app/core/openai/strict_schema.py +199 -0
- app/core/openai/tool_call_safety.py +99 -0
- app/core/openai/v1_requests.py +187 -0
- app/core/plan_types.py +90 -0
- app/core/prestop.py +202 -0
- app/core/rate_limiter/__init__.py +0 -0
- app/core/rate_limiter/db_rate_limiter.py +215 -0
- app/core/request_locality.py +473 -0
- app/core/resilience/__init__.py +0 -0
- app/core/resilience/backpressure.py +57 -0
- app/core/resilience/bulkhead.py +202 -0
- app/core/resilience/circuit_breaker.py +150 -0
- app/core/resilience/degradation.py +55 -0
- app/core/resilience/loop_lag_monitor.py +60 -0
- app/core/resilience/memory_monitor.py +137 -0
- app/core/resilience/network_recovery.py +256 -0
- app/core/resilience/overload.py +98 -0
- app/core/resilience/toggles.py +114 -0
- app/core/retention/__init__.py +0 -0
- app/core/retention/job.py +240 -0
- app/core/retention/scheduler.py +69 -0
- app/core/runtime_logging.py +601 -0
- app/core/scheduling/__init__.py +0 -0
- app/core/scheduling/leader_election.py +993 -0
- app/core/scheduling/leader_election_handle.py +25 -0
- app/core/server.py +140 -0
- app/core/shutdown.py +326 -0
- app/core/socket_peer.py +22 -0
- app/core/startup.py +41 -0
- app/core/timeout_invariants.py +347 -0
- app/core/tracing/__init__.py +0 -0
- app/core/tracing/otel.py +207 -0
- app/core/types.py +6 -0
- app/core/upstream_proxy/__init__.py +13 -0
- app/core/upstream_proxy/cache.py +130 -0
- app/core/upstream_proxy/resolver.py +189 -0
- app/core/upstream_proxy/types.py +104 -0
- app/core/usage/__init__.py +407 -0
- app/core/usage/depletion.py +109 -0
- app/core/usage/live_hub.py +41 -0
- app/core/usage/live_snapshots.py +174 -0
- app/core/usage/logs.py +212 -0
- app/core/usage/metadata_scheduler.py +118 -0
- app/core/usage/models.py +54 -0
- app/core/usage/pricing.py +576 -0
- app/core/usage/pricing_catalog.py +241 -0
- app/core/usage/pricing_snapshot.json +821 -0
- app/core/usage/quota.py +133 -0
- app/core/usage/refresh_policy.py +21 -0
- app/core/usage/refresh_scheduler.py +713 -0
- app/core/usage/reset_credits_refresh_scheduler.py +435 -0
- app/core/usage/types.py +168 -0
- app/core/utils/__init__.py +0 -0
- app/core/utils/json_guards.py +18 -0
- app/core/utils/locks.py +33 -0
- app/core/utils/masking.py +20 -0
- app/core/utils/proxy_env.py +104 -0
- app/core/utils/request_id.py +65 -0
- app/core/utils/retry.py +77 -0
- app/core/utils/shared_future.py +172 -0
- app/core/utils/sse.py +286 -0
- app/core/utils/time.py +24 -0
- app/db/__init__.py +0 -0
- app/db/account_identity_lock.py +56 -0
- app/db/alembic/__init__.py +1 -0
- app/db/alembic/env.py +70 -0
- app/db/alembic/revision_ids.py +43 -0
- app/db/alembic/versions/20260213_000000_base_schema.py +248 -0
- app/db/alembic/versions/20260213_000100_normalize_account_plan_types.py +45 -0
- app/db/alembic/versions/20260213_000200_add_request_logs_reasoning_effort.py +45 -0
- app/db/alembic/versions/20260213_000300_add_accounts_reset_at.py +45 -0
- app/db/alembic/versions/20260213_000400_add_accounts_chatgpt_account_id.py +45 -0
- app/db/alembic/versions/20260213_000500_add_dashboard_settings.py +70 -0
- app/db/alembic/versions/20260213_000600_add_dashboard_settings_totp.py +64 -0
- app/db/alembic/versions/20260213_000700_add_dashboard_settings_password.py +59 -0
- app/db/alembic/versions/20260213_000800_add_api_keys.py +87 -0
- app/db/alembic/versions/20260214_000000_add_api_key_limits.py +143 -0
- app/db/alembic/versions/20260215_000000_add_idx_logs_requested_at.py +38 -0
- app/db/alembic/versions/20260218_000000_add_api_key_usage_reservations.py +117 -0
- app/db/alembic/versions/20260218_000100_add_import_without_overwrite_and_drop_accounts_email_unique.py +198 -0
- app/db/alembic/versions/20260225_000000_add_dashboard_settings_routing_strategy.py +53 -0
- app/db/alembic/versions/20260228_020000_align_api_key_limit_enum_types.py +104 -0
- app/db/alembic/versions/20260228_030000_add_api_firewall_allowlist.py +42 -0
- app/db/alembic/versions/20260306_000000_add_request_logs_service_tier.py +45 -0
- app/db/alembic/versions/20260307_000000_add_api_key_enforcement_fields.py +56 -0
- app/db/alembic/versions/20260308_000000_add_sqlite_performance_indexes.py +41 -0
- app/db/alembic/versions/20260309_000000_add_additional_usage_history.py +71 -0
- app/db/alembic/versions/20260309_000000_request_logs_nullable_account_id.py +28 -0
- app/db/alembic/versions/20260310_000000_add_request_logs_transport.py +45 -0
- app/db/alembic/versions/20260310_000000_fix_postgresql_enum_value_casing.py +79 -0
- app/db/alembic/versions/20260310_120000_add_sticky_session_kinds_and_affinity_ttl.py +110 -0
- app/db/alembic/versions/20260312_000000_add_additional_usage_quota_key.py +193 -0
- app/db/alembic/versions/20260312_000000_split_sticky_sessions_primary_key_by_kind.py +96 -0
- app/db/alembic/versions/20260312_010000_merge_additional_usage_and_sticky_session_heads.py +25 -0
- app/db/alembic/versions/20260312_020000_merge_request_logs_transport_and_feature_heads.py +25 -0
- app/db/alembic/versions/20260312_020000_merge_request_logs_transport_and_sticky_heads.py +25 -0
- app/db/alembic/versions/20260312_120000_add_dashboard_upstream_stream_transport.py +62 -0
- app/db/alembic/versions/20260313_024500_merge_request_log_transport_heads.py +27 -0
- app/db/alembic/versions/20260319_100937_increase_prompt_cache_ttl_to_1800s.py +73 -0
- app/db/alembic/versions/20260319_111500_merge_prompt_cache_ttl_and_upstream_transport_heads.py +25 -0
- app/db/alembic/versions/20260319_183000_normalize_sqlite_account_status_casing.py +48 -0
- app/db/alembic/versions/20260320_000000_add_request_log_requested_actual_tiers.py +51 -0
- app/db/alembic/versions/20260321_190000_tighten_dashboard_db_indexes.py +64 -0
- app/db/alembic/versions/20260321_210000_merge_request_log_tiers_and_dashboard_index_heads.py +25 -0
- app/db/alembic/versions/20260325_000000_add_request_log_cost.py +193 -0
- app/db/alembic/versions/20260328_000000_add_rate_limit_attempts.py +48 -0
- app/db/alembic/versions/20260328_120000_add_request_log_ttft.py +37 -0
- app/db/alembic/versions/20260328_130000_add_audit_logs_table.py +45 -0
- app/db/alembic/versions/20260328_140000_add_scheduler_leader_table.py +39 -0
- app/db/alembic/versions/20260330_000000_add_cache_locality_settings.py +69 -0
- app/db/alembic/versions/20260330_010000_merge_scheduler_leader_and_cache_locality_heads.py +25 -0
- app/db/alembic/versions/20260330_020000_add_bridge_ring_members.py +46 -0
- app/db/alembic/versions/20260401_000000_add_api_key_enforced_service_tier.py +52 -0
- app/db/alembic/versions/20260401_000000_add_cache_invalidation.py +41 -0
- app/db/alembic/versions/20260401_020000_merge_cache_invalidation_and_api_key_service_tier_heads.py +22 -0
- app/db/alembic/versions/20260402_000000_switch_dashboard_routing_default_to_capacity_weighted.py +66 -0
- app/db/alembic/versions/20260403_000000_add_credit_api_key_limit_values.py +44 -0
- app/db/alembic/versions/20260406_000000_add_api_key_account_assignments.py +57 -0
- app/db/alembic/versions/20260406_010000_add_api_key_assignment_scope_flag.py +75 -0
- app/db/alembic/versions/20260407_000000_add_bridge_gateway_safe_mode.py +51 -0
- app/db/alembic/versions/20260407_010000_merge_api_key_assignment_and_bridge_gateway_heads.py +25 -0
- app/db/alembic/versions/20260408_000000_switch_import_without_overwrite_default_to_true.py +73 -0
- app/db/alembic/versions/20260408_010000_merge_import_without_overwrite_and_assignment_heads.py +25 -0
- app/db/alembic/versions/20260409_000000_add_http_bridge_sessions.py +130 -0
- app/db/alembic/versions/20260409_000000_switch_sticky_threads_and_prefer_earlier_reset_defaults_to_true.py +89 -0
- app/db/alembic/versions/20260409_010000_add_dashboard_settings_bootstrap_token.py +57 -0
- app/db/alembic/versions/20260409_010000_merge_http_bridge_and_import_without_overwrite_heads.py +25 -0
- app/db/alembic/versions/20260409_020000_fix_http_bridge_last_seen_index.py +46 -0
- app/db/alembic/versions/20260410_000000_backfill_pristine_dashboard_settings_defaults.py +77 -0
- app/db/alembic/versions/20260410_030000_restore_import_without_overwrite_default_true.py +73 -0
- app/db/alembic/versions/20260410_040000_merge_dashboard_defaults_and_import_default_heads.py +25 -0
- app/db/alembic/versions/20260413_000000_add_accounts_blocked_at.py +44 -0
- app/db/alembic/versions/20260415_160000_add_automations_tables.py +189 -0
- app/db/alembic/versions/20260415_160000_add_request_logs_response_lookup_index.py +67 -0
- app/db/alembic/versions/20260417_000000_add_request_log_plan_type.py +44 -0
- app/db/alembic/versions/20260418_190000_add_automation_runs_cycle_key.py +87 -0
- app/db/alembic/versions/20260419_000000_add_automation_run_cycle_metadata.py +56 -0
- app/db/alembic/versions/20260419_020000_add_automation_run_cycles_snapshot_tables.py +342 -0
- app/db/alembic/versions/20260421_000000_add_dashboard_session_ttl_setting.py +51 -0
- app/db/alembic/versions/20260421_120000_merge_request_log_lookup_and_plan_type_heads.py +25 -0
- app/db/alembic/versions/20260421_130000_merge_automation_and_request_log_heads.py +25 -0
- app/db/alembic/versions/20260422_103000_normalize_legacy_automation_run_cycle_keys.py +648 -0
- app/db/alembic/versions/20260423_120000_add_api_key_limit_reset_at_index.py +43 -0
- app/db/alembic/versions/20260424_000000_merge_dashboard_session_ttl_and_request_log_heads.py +25 -0
- app/db/alembic/versions/20260426_000000_add_dashboard_relative_availability_settings.py +68 -0
- app/db/alembic/versions/20260427_130000_add_warmup_model_and_request_kind.py +75 -0
- app/db/alembic/versions/20260506_000000_add_account_routing_policy.py +37 -0
- app/db/alembic/versions/20260509_000000_add_api_key_traffic_class.py +37 -0
- app/db/alembic/versions/20260509_010000_add_additional_quota_routing_policies.py +37 -0
- app/db/alembic/versions/20260513_000000_add_accounts_alias.py +44 -0
- app/db/alembic/versions/20260513_000000_add_api_key_apply_to_codex_model.py +59 -0
- app/db/alembic/versions/20260514_000000_add_request_logs_api_key_time_index.py +43 -0
- app/db/alembic/versions/20260515_000000_add_prefer_earlier_reset_window.py +47 -0
- app/db/alembic/versions/20260515_000000_soft_delete_request_logs_on_account_delete.py +83 -0
- app/db/alembic/versions/20260515_020000_add_split_sticky_budget_thresholds.py +62 -0
- app/db/alembic/versions/20260516_000000_add_sqlite_hot_path_indexes.py +75 -0
- app/db/alembic/versions/20260517_000000_merge_sqlite_hot_path_and_request_log_delete_heads.py +24 -0
- app/db/alembic/versions/20260518_000000_add_http_bridge_durable_input_prefix.py +44 -0
- app/db/alembic/versions/20260518_010000_merge_http_bridge_and_sqlite_recovery_heads.py +25 -0
- app/db/alembic/versions/20260518_120000_add_dashboard_guest_access.py +57 -0
- app/db/alembic/versions/20260520_000000_merge_api_key_and_http_bridge_heads.py +26 -0
- app/db/alembic/versions/20260520_010000_add_request_logs_api_key_account_index.py +45 -0
- app/db/alembic/versions/20260520_010000_merge_automation_and_api_key_heads.py +26 -0
- app/db/alembic/versions/20260520_030000_add_quota_planner.py +194 -0
- app/db/alembic/versions/20260520_040000_merge_quota_planner_and_request_log_index_heads.py +24 -0
- app/db/alembic/versions/20260521_000000_add_account_security_work_authorized.py +46 -0
- app/db/alembic/versions/20260521_000000_merge_dashboard_guest_and_api_key_heads.py +26 -0
- app/db/alembic/versions/20260521_000000_merge_prefer_reset_window_head.py +26 -0
- app/db/alembic/versions/20260521_000000_merge_warmup_and_request_log_index_heads.py +25 -0
- app/db/alembic/versions/20260522_000000_add_limit_warmup_trigger.py +195 -0
- app/db/alembic/versions/20260524_000000_merge_accounts_alias_and_reset_window_heads.py +24 -0
- app/db/alembic/versions/20260525_000000_add_usage_raw_window_latest_index.py +31 -0
- app/db/alembic/versions/20260526_000000_add_request_log_failure_metadata.py +100 -0
- app/db/alembic/versions/20260531_000000_add_account_workspace_identity.py +39 -0
- app/db/alembic/versions/20260531_000000_add_single_account_routing.py +42 -0
- app/db/alembic/versions/20260601_000000_merge_relative_availability_and_usage_raw_heads.py +24 -0
- app/db/alembic/versions/20260601_010000_add_api_key_usage_sections.py +58 -0
- app/db/alembic/versions/20260601_010000_add_routing_policy_persistence.py +67 -0
- app/db/alembic/versions/20260601_010000_merge_security_work_and_relative_availability_heads.py +25 -0
- app/db/alembic/versions/20260601_010000_merge_split_sticky_and_relative_usage_heads.py +25 -0
- app/db/alembic/versions/20260601_010000_merge_warmup_and_relative_availability_heads.py +25 -0
- app/db/alembic/versions/20260601_020000_merge_additional_quota_routing_and_relative_availability_heads.py +25 -0
- app/db/alembic/versions/20260601_020000_merge_quota_planner_and_relative_availability_heads.py +25 -0
- app/db/alembic/versions/20260601_020000_merge_reset_window_and_relative_availability_heads.py +25 -0
- app/db/alembic/versions/20260601_020000_merge_routing_policy_and_split_threshold_heads.py +24 -0
- app/db/alembic/versions/20260601_020000_merge_warmup_and_request_log_failure_heads.py +25 -0
- app/db/alembic/versions/20260601_030000_merge_single_account_and_routing_quota_heads.py +29 -0
- app/db/alembic/versions/20260601_040000_merge_security_work_and_routing_consolidation_heads.py +25 -0
- app/db/alembic/versions/20260601_050000_merge_quota_planner_and_routing_consolidation_heads.py +25 -0
- app/db/alembic/versions/20260602_050000_add_upstream_proxy_routing.py +164 -0
- app/db/alembic/versions/20260602_060000_merge_account_workspace_and_failure_heads.py +25 -0
- app/db/alembic/versions/20260602_070000_add_request_log_useragent_fields.py +86 -0
- app/db/alembic/versions/20260602_070000_merge_quota_planner_and_account_workspace_heads.py +25 -0
- app/db/alembic/versions/20260602_080000_merge_upstream_proxy_and_quota_planner_heads.py +25 -0
- app/db/alembic/versions/20260602_080000_merge_useragent_and_upstream_proxy_heads.py +24 -0
- app/db/alembic/versions/20260603_000000_add_weekly_pace_working_days.py +52 -0
- app/db/alembic/versions/20260603_000000_free_account_monthly_window.py +60 -0
- app/db/alembic/versions/20260604_000000_add_reauth_required_account_status.py +159 -0
- app/db/alembic/versions/20260604_010000_merge_automations_and_reauth_status_heads.py +25 -0
- app/db/alembic/versions/20260604_010000_merge_dashboard_guest_and_reauth_status_heads.py +25 -0
- app/db/alembic/versions/20260604_010000_merge_useragent_and_reauth_heads.py +24 -0
- app/db/alembic/versions/20260607_000000_merge_weekly_monthly_useragent_heads.py +28 -0
- app/db/alembic/versions/20260608_000000_add_hide_upstream_quota_from_api_keys.py +50 -0
- app/db/alembic/versions/20260611_000000_add_request_log_archive_request_id.py +47 -0
- app/db/alembic/versions/20260611_000000_merge_automations_and_weekly_monthly_useragent_heads.py +26 -0
- app/db/alembic/versions/20260611_000000_merge_dashboard_guest_and_weekly_useragent_heads.py +26 -0
- app/db/alembic/versions/20260611_010000_add_request_log_client_ip.py +57 -0
- app/db/alembic/versions/20260611_020000_merge_request_log_archive_and_client_ip_heads.py +26 -0
- app/db/alembic/versions/20260611_030000_merge_dashboard_guest_and_request_log_client_ip_heads.py +26 -0
- app/db/alembic/versions/20260613_000000_add_accounts_codex_installation_id.py +57 -0
- app/db/alembic/versions/20260613_010000_merge_installation_id_and_dashboard_weekly_heads.py +26 -0
- app/db/alembic/versions/20260615_000000_merge_automations_and_dashboard_guest_heads.py +26 -0
- app/db/alembic/versions/20260623_000000_add_limit_warmup_exhausted_threshold.py +51 -0
- app/db/alembic/versions/20260626_000000_add_http_downstream_transport_policy.py +46 -0
- app/db/alembic/versions/20260626_010000_add_request_logs_upstream_transport.py +31 -0
- app/db/alembic/versions/20260629_000000_add_dashboard_query_hot_path_indexes.py +53 -0
- app/db/alembic/versions/20260630_000000_add_limit_warmup_staggered_idle.py +47 -0
- app/db/alembic/versions/20260630_000000_merge_request_log_client_ip_and_dashboard_hot_path_heads.py +26 -0
- app/db/alembic/versions/20260630_010000_merge_warmup_and_request_log_dashboard_heads.py +26 -0
- app/db/alembic/versions/20260630_020000_merge_automations_and_main_heads.py +26 -0
- app/db/alembic/versions/20260630_020000_merge_warmup_threshold_and_main_heads.py +26 -0
- app/db/alembic/versions/20260630_030000_add_automation_run_model_snapshot.py +59 -0
- app/db/alembic/versions/20260630_040000_merge_automation_snapshot_and_warmup_threshold_heads.py +26 -0
- app/db/alembic/versions/20260630_050000_add_automation_run_prompt_snapshot.py +52 -0
- app/db/alembic/versions/20260701_000000_add_weekly_pace_smoothing_minutes.py +51 -0
- app/db/alembic/versions/20260702_000000_add_model_sources.py +176 -0
- app/db/alembic/versions/20260705_000000_add_model_source_audio_transcriptions.py +50 -0
- app/db/alembic/versions/20260705_000000_harden_dashboard_session_ttl.py +62 -0
- app/db/alembic/versions/20260706_000000_add_model_source_audio_per_minute.py +43 -0
- app/db/alembic/versions/20260707_000000_merge_model_sources_and_session_ttl.py +24 -0
- app/db/alembic/versions/20260707_010000_merge_automations_and_model_sources_heads.py +24 -0
- app/db/alembic/versions/20260707_020000_add_automation_job_account_scope.py +125 -0
- app/db/alembic/versions/20260709_000000_add_ttft_phase_observability.py +66 -0
- app/db/alembic/versions/20260711_000000_add_dashboard_prohibit_fast_mode.py +42 -0
- app/db/alembic/versions/20260711_010000_add_account_chatgpt_user_id.py +40 -0
- app/db/alembic/versions/20260711_020000_add_dashboard_account_concurrency_caps.py +71 -0
- app/db/alembic/versions/20260711_030000_add_limit_warmup_idle_threshold.py +51 -0
- app/db/alembic/versions/20260712_010000_add_account_usage_rollups.py +56 -0
- app/db/alembic/versions/20260712_020000_add_api_key_usage_rollups.py +53 -0
- app/db/alembic/versions/20260713_020000_add_model_registry_snapshot.py +42 -0
- app/db/alembic/versions/20260713_040000_add_account_refresh_claims.py +42 -0
- app/db/alembic/versions/20260713_040000_add_replica_guardrails.py +64 -0
- app/db/alembic/versions/20260713_070000_add_reset_credit_redeem_tables.py +58 -0
- app/db/alembic/versions/20260714_000000_add_oauth_flow_states.py +63 -0
- app/db/alembic/versions/20260715_000000_add_request_log_queue_latency.py +44 -0
- app/db/alembic/versions/20260716_000000_add_oauth_device_flow_slots.py +41 -0
- app/db/alembic/versions/20260716_010000_add_dashboard_retention_settings.py +48 -0
- app/db/alembic/versions/20260716_010000_add_reset_credit_display_settings.py +73 -0
- app/db/alembic/versions/20260717_000000_add_http_bridge_retry_circuits.py +47 -0
- app/db/alembic/versions/20260717_000000_merge_retention_and_reset_credit_display_heads.py +26 -0
- app/db/alembic/versions/20260717_000000_optimize_dashboard_hot_path_indexes.py +162 -0
- app/db/alembic/versions/20260717_000001_merge_retry_circuits_and_dashboard_indexes.py +22 -0
- app/db/alembic/versions/20260720_000000_add_request_log_conversation_id.py +57 -0
- app/db/alembic/versions/20260722_000000_backfill_request_log_useragent_families.py +40 -0
- app/db/alembic/versions/20260724_000000_add_request_usage_time_rollups.py +176 -0
- app/db/alembic/versions/20260724_000000_merge_request_log_schema_heads.py +33 -0
- app/db/alembic/versions/20260725_000000_add_http_bridge_pending_tool_calls.py +43 -0
- app/db/alembic/versions/20260726_000000_add_account_plan_downgrade_observations.py +44 -0
- app/db/alembic/versions/20260726_000000_repair_request_usage_rollups_after_merge.py +37 -0
- app/db/alembic/versions/20260727_000000_add_sticky_session_continuity_abandoned_at.py +45 -0
- app/db/alembic/versions/20260728_000000_merge_pending_tool_calls_and_rollup_repair_heads.py +31 -0
- app/db/alembic/versions/20260730_000000_add_api_key_fair_share_threshold.py +44 -0
- app/db/alembic/versions/20260730_000000_add_http_bridge_recovery_attempts.py +60 -0
- app/db/alembic/versions/20260731_000000_add_capability_lineage_markers.py +38 -0
- app/db/alembic/versions/20260803_000000_merge_http_bridge_recovery_and_capability_lineage_heads.py +30 -0
- app/db/alembic/versions/20260804_000000_add_http_bridge_operations.py +63 -0
- app/db/alembic/versions/20260804_000001_add_global_http_bridge_operation_fingerprint.py +53 -0
- app/db/alembic/versions/20260804_230000_add_request_log_connection_request_kind.py +43 -0
- app/db/alembic/versions/20260805_000000_add_http_bridge_operation_spool.py +75 -0
- app/db/alembic/versions/20260805_000001_finalize_http_bridge_operation_spool.py +65 -0
- app/db/alembic/versions/20260806_000000_add_additional_usage_alias_probe_indexes.py +84 -0
- app/db/alembic/versions/20260806_000000_add_anonymous_telemetry.py +49 -0
- app/db/alembic/versions/20260806_010000_add_conversation_presence_rollup.py +72 -0
- app/db/alembic/versions/20260806_020000_add_usage_history_bulk_covering_indexes.py +99 -0
- app/db/alembic/versions/20260806_030000_add_api_key_allowed_reasoning_efforts.py +65 -0
- app/db/alembic/versions/20260806_120000_add_http_bridge_owner_process_epoch.py +57 -0
- app/db/alembic/versions/20260807_000000_merge_http_bridge_operation_spool_and_latest_main.py +24 -0
- app/db/alembic/versions/20260808_000000_tune_usage_history_autovacuum.py +54 -0
- app/db/alembic/versions/20260810_000000_add_http_bridge_recovery_dispatch_count.py +44 -0
- app/db/alembic/versions/20260811_000000_add_hourly_rollup_cancelled_count.py +92 -0
- app/db/alembic/versions/20260812_000000_merge_recovery_dispatch_and_hourly_cancelled_heads.py +25 -0
- app/db/alembic/versions/20260812_120000_add_sticky_abandonment_scope.py +43 -0
- app/db/alembic/versions/20260813_000000_add_file_account_pins.py +40 -0
- app/db/alembic/versions/20260816_000000_add_account_pending_deletion.py +81 -0
- app/db/alembic/versions/20260816_000000_add_model_source_embeddings.py +50 -0
- app/db/alembic/versions/20260821_000000_add_retry_circuit_admission_generation.py +49 -0
- app/db/alembic/versions/20260826_000000_add_http_bridge_event_chunks.py +107 -0
- app/db/alembic/versions/20260828_000000_add_accounts_chatgpt_identity_index.py +55 -0
- app/db/alembic/versions/20260830_000000_add_quota_warmup_claim_expiry.py +83 -0
- app/db/alembic/versions/20260908_000000_add_guest_session_generation.py +37 -0
- app/db/alembic/versions/20260908_000000_add_subscription_overflow.py +140 -0
- app/db/alembic/versions/20260908_000000_replace_upstream_stream_transport_default_sentinel.py +50 -0
- app/db/alembic/versions/20260908_020000_merge_overflow_transport_heads.py +27 -0
- app/db/alembic/versions/20260909_000000_add_dashboard_roles.py +72 -0
- app/db/alembic/versions/20260909_010000_add_dashboard_users.py +183 -0
- app/db/alembic/versions/20260909_020000_reproject_compat_admin_credentials.py +105 -0
- app/db/alembic/versions/20260909_030000_add_audit_actor_columns.py +103 -0
- app/db/alembic/versions/20260909_030000_dashboard_resilience_toggle_settings.py +63 -0
- app/db/alembic/versions/20260909_040000_add_dashboard_user_invites.py +54 -0
- app/db/alembic/versions/20260909_040000_dashboard_timeout_settings.py +68 -0
- app/db/alembic/versions/20260909_050000_dashboard_routing_overload_settings.py +65 -0
- app/db/alembic/versions/20260909_060000_add_report_rollup.py +74 -0
- app/db/alembic/versions/20260909_070000_automation_run_claim_budget.py +50 -0
- app/db/alembic/versions/20260909_080000_dashboard_stream_bridge_budgets.py +62 -0
- app/db/alembic/versions/20260909_090000_dashboard_background_job_toggles.py +63 -0
- app/db/alembic/versions/20260909_100000_dashboard_codex_prewarm.py +49 -0
- app/db/alembic/versions/20260909_110000_model_context_window_overrides.py +45 -0
- app/db/alembic/versions/20260909_120000_dashboard_conversation_archive.py +48 -0
- app/db/alembic/versions/20260909_130000_add_request_logs_live_facet_indexes.py +96 -0
- app/db/alembic/versions/20260910_000000_add_totp_required_for_admin_role.py +44 -0
- app/db/alembic/versions/20260910_000000_request_logs_missing_cost_index.py +52 -0
- app/db/alembic/versions/20260910_010000_add_dashboard_auth_providers.py +134 -0
- app/db/alembic/versions/20260910_010000_dashboard_spool_retention.py +52 -0
- app/db/alembic/versions/20260910_020000_add_dashboard_role_mappings.py +74 -0
- app/db/alembic/versions/20260910_143000_request_log_affinity.py +28 -0
- app/db/alembic/versions/20260910_180000_merge_affinity_guest_heads.py +17 -0
- app/db/alembic/versions/20260910_200000_merge_affinity_identity_heads.py +17 -0
- app/db/alembic/versions/20260910_220000_merge_affinity_invite_heads.py +17 -0
- app/db/alembic/versions/20260911_000000_model_source_pins_kind_expires_index.py +43 -0
- app/db/alembic/versions/20260911_010000_merge_pin_index_and_affinity_heads.py +17 -0
- app/db/alembic/versions/20260911_020000_add_http_bridge_terminal_append_phase.py +54 -0
- app/db/alembic/versions/20260911_030000_add_local_login_policy.py +48 -0
- app/db/alembic/versions/20260911_040000_add_thread_cache_identity_mode.py +48 -0
- app/db/alembic/versions/20260911_060000_add_bridge_session_continuity_abandonment.py +40 -0
- app/db/alembic/versions/20260912_000000_merge_thread_cache_and_bridge_retirement_heads.py +22 -0
- app/db/alembic/versions/20260912_010000_drop_legacy_dashboard_credentials.py +140 -0
- app/db/alembic/versions/20260913_000000_add_oidc_provider_flow.py +134 -0
- app/db/alembic/versions/20260914_000000_add_scim_tokens.py +78 -0
- app/db/alembic/versions/20260914_000000_drop_subscription_overflow_schema.py +132 -0
- app/db/alembic/versions/20260918_000000_merge_scim_and_overflow_heads.py +17 -0
- app/db/alembic/versions/__init__.py +1 -0
- app/db/backup.py +64 -0
- app/db/migrate.py +1249 -0
- app/db/migration_lock.py +160 -0
- app/db/migration_url.py +18 -0
- app/db/models.py +3100 -0
- app/db/recover.py +244 -0
- app/db/session.py +1295 -0
- app/db/snapshot.py +46 -0
- app/db/sqlite_lock_retry.py +190 -0
- app/db/sqlite_utils.py +549 -0
- app/dependencies.py +462 -0
- app/main.py +1240 -0
- app/modules/__init__.py +0 -0
- app/modules/accounts/__init__.py +0 -0
- app/modules/accounts/api.py +426 -0
- app/modules/accounts/auth_manager.py +1439 -0
- app/modules/accounts/background_repository.py +160 -0
- app/modules/accounts/deletion.py +540 -0
- app/modules/accounts/mappers.py +654 -0
- app/modules/accounts/refresh_claims.py +352 -0
- app/modules/accounts/repository.py +1679 -0
- app/modules/accounts/schemas.py +308 -0
- app/modules/accounts/service.py +809 -0
- app/modules/accounts/usage_rollup.py +486 -0
- app/modules/accounts/usage_rollup_scheduler.py +75 -0
- app/modules/accounts/usage_time_rollup.py +1037 -0
- app/modules/accounts/usage_time_rollup_read.py +522 -0
- app/modules/api_keys/__init__.py +1 -0
- app/modules/api_keys/api.py +344 -0
- app/modules/api_keys/last_used_coalescer.py +216 -0
- app/modules/api_keys/limit_windows.py +41 -0
- app/modules/api_keys/repository.py +1200 -0
- app/modules/api_keys/reset_scheduler.py +85 -0
- app/modules/api_keys/schemas.py +135 -0
- app/modules/api_keys/service.py +2123 -0
- app/modules/audit/__init__.py +0 -0
- app/modules/audit/api.py +88 -0
- app/modules/audit/repository.py +74 -0
- app/modules/audit/schemas.py +33 -0
- app/modules/audit/service.py +71 -0
- app/modules/auth_providers/__init__.py +0 -0
- app/modules/auth_providers/api.py +127 -0
- app/modules/auth_providers/config.py +343 -0
- app/modules/auth_providers/repository.py +101 -0
- app/modules/auth_providers/schemas.py +88 -0
- app/modules/auth_providers/seed.py +105 -0
- app/modules/auth_providers/service.py +289 -0
- app/modules/automations/__init__.py +1 -0
- app/modules/automations/api.py +369 -0
- app/modules/automations/repository.py +1896 -0
- app/modules/automations/scheduler.py +107 -0
- app/modules/automations/schemas.py +158 -0
- app/modules/automations/service.py +2548 -0
- app/modules/cache_isolation_probe/__init__.py +0 -0
- app/modules/cache_isolation_probe/api.py +188 -0
- app/modules/cache_isolation_probe/prefix.py +144 -0
- app/modules/cache_isolation_probe/schemas.py +94 -0
- app/modules/cache_isolation_probe/sender.py +229 -0
- app/modules/cache_isolation_probe/service.py +525 -0
- app/modules/conversation_archive/__init__.py +1 -0
- app/modules/conversation_archive/api.py +101 -0
- app/modules/conversation_archive/schemas.py +28 -0
- app/modules/conversation_archive/service.py +165 -0
- app/modules/dashboard/__init__.py +1 -0
- app/modules/dashboard/api.py +106 -0
- app/modules/dashboard/builders.py +107 -0
- app/modules/dashboard/repository.py +304 -0
- app/modules/dashboard/schemas.py +148 -0
- app/modules/dashboard/service.py +534 -0
- app/modules/dashboard/timeframes.py +19 -0
- app/modules/dashboard/weekly_pace.py +617 -0
- app/modules/dashboard_auth/__init__.py +1 -0
- app/modules/dashboard_auth/api.py +1344 -0
- app/modules/dashboard_auth/oidc_api.py +753 -0
- app/modules/dashboard_auth/oidc_flows.py +330 -0
- app/modules/dashboard_auth/repository.py +452 -0
- app/modules/dashboard_auth/schemas.py +232 -0
- app/modules/dashboard_auth/service.py +1384 -0
- app/modules/dashboard_roles/__init__.py +0 -0
- app/modules/dashboard_roles/api.py +64 -0
- app/modules/dashboard_roles/repository.py +54 -0
- app/modules/dashboard_roles/schemas.py +29 -0
- app/modules/dashboard_roles/seed.py +80 -0
- app/modules/dashboard_roles/service.py +142 -0
- app/modules/dashboard_users/__init__.py +0 -0
- app/modules/dashboard_users/api.py +278 -0
- app/modules/dashboard_users/break_glass.py +169 -0
- app/modules/dashboard_users/credentials.py +18 -0
- app/modules/dashboard_users/identity_resolver.py +665 -0
- app/modules/dashboard_users/repository.py +725 -0
- app/modules/dashboard_users/schemas.py +117 -0
- app/modules/dashboard_users/service.py +1054 -0
- app/modules/firewall/__init__.py +1 -0
- app/modules/firewall/api.py +78 -0
- app/modules/firewall/repository.py +51 -0
- app/modules/firewall/schemas.py +28 -0
- app/modules/firewall/service.py +93 -0
- app/modules/fleet/__init__.py +1 -0
- app/modules/fleet/api.py +177 -0
- app/modules/fleet/mappers.py +56 -0
- app/modules/fleet/observability.py +372 -0
- app/modules/fleet/schemas.py +119 -0
- app/modules/health/__init__.py +0 -0
- app/modules/health/api.py +246 -0
- app/modules/health/schemas.py +27 -0
- app/modules/limit_warmup/__init__.py +1 -0
- app/modules/limit_warmup/repository.py +159 -0
- app/modules/limit_warmup/service.py +1001 -0
- app/modules/model_sources/__init__.py +1 -0
- app/modules/model_sources/api.py +99 -0
- app/modules/model_sources/catalog.py +307 -0
- app/modules/model_sources/forwarding.py +1435 -0
- app/modules/model_sources/projection.py +55 -0
- app/modules/model_sources/repository.py +201 -0
- app/modules/model_sources/schemas.py +79 -0
- app/modules/model_sources/selection.py +166 -0
- app/modules/model_sources/service.py +236 -0
- app/modules/oauth/__init__.py +0 -0
- app/modules/oauth/api.py +101 -0
- app/modules/oauth/repository.py +238 -0
- app/modules/oauth/schemas.py +49 -0
- app/modules/oauth/service.py +1196 -0
- app/modules/oauth/templates/oauth_success.html +122 -0
- app/modules/proxy/__init__.py +0 -0
- app/modules/proxy/_load_balancer/__init__.py +1 -0
- app/modules/proxy/_load_balancer/error_rate.py +116 -0
- app/modules/proxy/_load_balancer/latency_cohort.py +154 -0
- app/modules/proxy/_load_balancer/model_eligibility.py +250 -0
- app/modules/proxy/_load_balancer/opportunistic_admission.py +350 -0
- app/modules/proxy/_load_balancer/overload_backoff.py +394 -0
- app/modules/proxy/_load_balancer/sticky_selection.py +2417 -0
- app/modules/proxy/_load_balancer/throughput_cohort.py +233 -0
- app/modules/proxy/_load_balancer/ttft_cohort.py +214 -0
- app/modules/proxy/_load_balancer/tunables.py +97 -0
- app/modules/proxy/_load_balancer/types.py +96 -0
- app/modules/proxy/_load_balancer/unbound_selection.py +534 -0
- app/modules/proxy/_service/__init__.py +6 -0
- app/modules/proxy/_service/api_key_usage.py +745 -0
- app/modules/proxy/_service/clock_budget.py +38 -0
- app/modules/proxy/_service/codex_control.py +611 -0
- app/modules/proxy/_service/compact.py +2211 -0
- app/modules/proxy/_service/file_ops.py +749 -0
- app/modules/proxy/_service/http_bridge/__init__.py +3 -0
- app/modules/proxy/_service/http_bridge/accepted_replay.py +331 -0
- app/modules/proxy/_service/http_bridge/account_sessions.py +57 -0
- app/modules/proxy/_service/http_bridge/activity.py +178 -0
- app/modules/proxy/_service/http_bridge/helpers.py +3959 -0
- app/modules/proxy/_service/http_bridge/mixin.py +2435 -0
- app/modules/proxy/_service/http_bridge/owner_forwarding.py +609 -0
- app/modules/proxy/_service/http_bridge/protocol.py +167 -0
- app/modules/proxy/_service/http_bridge/proxy_failover.py +58 -0
- app/modules/proxy/_service/http_bridge/quarantine.py +443 -0
- app/modules/proxy/_service/http_bridge/request_submit.py +4582 -0
- app/modules/proxy/_service/http_bridge/retry_circuit.py +2442 -0
- app/modules/proxy/_service/http_bridge/service_stubs.py +492 -0
- app/modules/proxy/_service/http_bridge/session_registry.py +778 -0
- app/modules/proxy/_service/http_bridge/streaming.py +5328 -0
- app/modules/proxy/_service/http_bridge/upstream_events.py +4538 -0
- app/modules/proxy/_service/observability.py +410 -0
- app/modules/proxy/_service/rate_limit.py +361 -0
- app/modules/proxy/_service/realtime_live.py +655 -0
- app/modules/proxy/_service/refresh.py +154 -0
- app/modules/proxy/_service/request_log.py +585 -0
- app/modules/proxy/_service/response_create.py +930 -0
- app/modules/proxy/_service/streaming/__init__.py +3 -0
- app/modules/proxy/_service/streaming/helpers.py +1317 -0
- app/modules/proxy/_service/streaming/mixin.py +1100 -0
- app/modules/proxy/_service/streaming/protocol.py +32 -0
- app/modules/proxy/_service/streaming/retry.py +3762 -0
- app/modules/proxy/_service/support.py +2122 -0
- app/modules/proxy/_service/transcribe.py +434 -0
- app/modules/proxy/_service/warmup.py +483 -0
- app/modules/proxy/_service/websocket/__init__.py +3 -0
- app/modules/proxy/_service/websocket/helpers.py +2360 -0
- app/modules/proxy/_service/websocket/mixin.py +7285 -0
- app/modules/proxy/_service/websocket/protocol.py +74 -0
- app/modules/proxy/account_cache.py +229 -0
- app/modules/proxy/account_eligibility.py +87 -0
- app/modules/proxy/additional_model_limits.py +40 -0
- app/modules/proxy/affinity.py +830 -0
- app/modules/proxy/affinity_observation.py +101 -0
- app/modules/proxy/api.py +10254 -0
- app/modules/proxy/api_key_usage.py +113 -0
- app/modules/proxy/cap_partitioning.py +280 -0
- app/modules/proxy/capability_lineage.py +65 -0
- app/modules/proxy/capability_lineage_repository.py +82 -0
- app/modules/proxy/capability_routing.py +253 -0
- app/modules/proxy/continuity.py +112 -0
- app/modules/proxy/downstream_delivery.py +277 -0
- app/modules/proxy/durable_bridge_coordinator.py +1217 -0
- app/modules/proxy/durable_bridge_repository.py +4516 -0
- app/modules/proxy/durable_bridge_runtime.py +9 -0
- app/modules/proxy/durable_bridge_transcript_codec.py +122 -0
- app/modules/proxy/fair_share.py +131 -0
- app/modules/proxy/file_pin_repository.py +210 -0
- app/modules/proxy/helpers.py +439 -0
- app/modules/proxy/http_bridge_event_batcher.py +613 -0
- app/modules/proxy/http_bridge_forwarding.py +797 -0
- app/modules/proxy/http_continuation.py +76 -0
- app/modules/proxy/images_observability.py +62 -0
- app/modules/proxy/images_service.py +1054 -0
- app/modules/proxy/load_balancer.py +3016 -0
- app/modules/proxy/rate_limit_cache.py +46 -0
- app/modules/proxy/replay_safety.py +1063 -0
- app/modules/proxy/repo_bundle.py +31 -0
- app/modules/proxy/request_policy.py +1054 -0
- app/modules/proxy/ring_membership.py +251 -0
- app/modules/proxy/schemas.py +345 -0
- app/modules/proxy/selection_errors.py +41 -0
- app/modules/proxy/service.py +2600 -0
- app/modules/proxy/source_admission.py +125 -0
- app/modules/proxy/source_dispatch.py +999 -0
- app/modules/proxy/sticky_repository.py +831 -0
- app/modules/proxy/thread_anchors.py +491 -0
- app/modules/proxy/tool_call_dedupe.py +802 -0
- app/modules/proxy/types.py +54 -0
- app/modules/proxy/work_admission.py +121 -0
- app/modules/quota_planner/__init__.py +1 -0
- app/modules/quota_planner/api.py +276 -0
- app/modules/quota_planner/logic.py +743 -0
- app/modules/quota_planner/repository.py +722 -0
- app/modules/quota_planner/scheduler.py +212 -0
- app/modules/quota_planner/schemas.py +102 -0
- app/modules/quota_planner/warmup.py +693 -0
- app/modules/rate_limit_reset_credits/__init__.py +0 -0
- app/modules/rate_limit_reset_credits/api.py +601 -0
- app/modules/rate_limit_reset_credits/redeem_coordination.py +274 -0
- app/modules/rate_limit_reset_credits/store.py +114 -0
- app/modules/reports/__init__.py +1 -0
- app/modules/reports/api.py +134 -0
- app/modules/reports/cache.py +74 -0
- app/modules/reports/filters.py +27 -0
- app/modules/reports/repository.py +525 -0
- app/modules/reports/rollup.py +150 -0
- app/modules/reports/rollup_read.py +148 -0
- app/modules/reports/schemas.py +123 -0
- app/modules/reports/service.py +287 -0
- app/modules/reports/thread_identity.py +256 -0
- app/modules/request_logs/__init__.py +0 -0
- app/modules/request_logs/api.py +224 -0
- app/modules/request_logs/cost_backfill.py +154 -0
- app/modules/request_logs/mappers.py +97 -0
- app/modules/request_logs/repository.py +1765 -0
- app/modules/request_logs/schemas.py +145 -0
- app/modules/request_logs/service.py +348 -0
- app/modules/role_mappings/__init__.py +0 -0
- app/modules/role_mappings/api.py +206 -0
- app/modules/role_mappings/matching.py +55 -0
- app/modules/role_mappings/repository.py +106 -0
- app/modules/role_mappings/schemas.py +67 -0
- app/modules/role_mappings/service.py +334 -0
- app/modules/runtime/__init__.py +1 -0
- app/modules/runtime/api.py +18 -0
- app/modules/runtime/schemas.py +14 -0
- app/modules/runtime/service.py +218 -0
- app/modules/scim/__init__.py +1 -0
- app/modules/scim/api.py +194 -0
- app/modules/scim/dependencies.py +112 -0
- app/modules/scim/errors.py +77 -0
- app/modules/scim/management_api.py +156 -0
- app/modules/scim/repository.py +212 -0
- app/modules/scim/schemas.py +145 -0
- app/modules/scim/service.py +786 -0
- app/modules/scim/tokens.py +43 -0
- app/modules/settings/__init__.py +1 -0
- app/modules/settings/api.py +1677 -0
- app/modules/settings/repository.py +606 -0
- app/modules/settings/schemas.py +496 -0
- app/modules/settings/service.py +769 -0
- app/modules/shared/__init__.py +0 -0
- app/modules/shared/schemas.py +22 -0
- app/modules/sticky_sessions/__init__.py +1 -0
- app/modules/sticky_sessions/api.py +118 -0
- app/modules/sticky_sessions/cleanup_scheduler.py +487 -0
- app/modules/sticky_sessions/schemas.py +78 -0
- app/modules/sticky_sessions/service.py +196 -0
- app/modules/telemetry/__init__.py +5 -0
- app/modules/telemetry/api.py +115 -0
- app/modules/telemetry/clients.py +62 -0
- app/modules/telemetry/consent.py +115 -0
- app/modules/telemetry/scheduler.py +85 -0
- app/modules/telemetry/schemas.py +163 -0
- app/modules/telemetry/sender.py +221 -0
- app/modules/telemetry/snapshot.py +490 -0
- app/modules/usage/__init__.py +0 -0
- app/modules/usage/additional_quota_keys.py +379 -0
- app/modules/usage/api.py +45 -0
- app/modules/usage/background_repository.py +147 -0
- app/modules/usage/builders.py +559 -0
- app/modules/usage/depletion_service.py +277 -0
- app/modules/usage/live_ingest.py +356 -0
- app/modules/usage/mappers.py +29 -0
- app/modules/usage/plan_downgrade_observations.py +488 -0
- app/modules/usage/repository.py +1890 -0
- app/modules/usage/schemas.py +68 -0
- app/modules/usage/service.py +102 -0
- app/modules/usage/updater.py +1488 -0
- app/static/assets/Trans-Cj-J9mVA.js +1 -0
- app/static/assets/access-page-DHzZiXZi.js +1 -0
- app/static/assets/account-cost-donut-DGhfA4U8.js +1 -0
- app/static/assets/account-identifiers-CxbdLshZ.js +1 -0
- app/static/assets/account-status-DIsU5sUS.js +1 -0
- app/static/assets/account-trend-chart-BptlfwwQ.js +1 -0
- app/static/assets/accounts-page-Bbai8ift.js +3 -0
- app/static/assets/api-Dwa7XxPe.js +1 -0
- app/static/assets/api-client-dugX1Pti.js +55 -0
- app/static/assets/api-key-create-dialog-DnRm0oYN.js +1 -0
- app/static/assets/api-key-created-dialog-B6jkDG9f.js +1 -0
- app/static/assets/api-key-edit-dialog-Ck3Bnc3V.js +1 -0
- app/static/assets/api-trend-chart-BchCw7Hj.js +1 -0
- app/static/assets/apis-page-BvXz9c6i.js +2 -0
- app/static/assets/arrow-up-Dqh_KzJo.js +1 -0
- app/static/assets/arrow-up-down-LBRgVDGY.js +1 -0
- app/static/assets/automations-page-DQgNAJOR.js +3 -0
- app/static/assets/badge-DqMb9es0.js +1 -0
- app/static/assets/blur-email-HEvKagG2.js +1 -0
- app/static/assets/button-BjVmmiGW.js +1 -0
- app/static/assets/checkbox-apsl9vzk.js +1 -0
- app/static/assets/chevron-left-1d_pHVa5.js +1 -0
- app/static/assets/circle-alert-BS0xd_o1.js +1 -0
- app/static/assets/clsx-DB0hHKMi.js +1 -0
- app/static/assets/colors-BBy8GEtr.js +1 -0
- app/static/assets/copy-button-D7nAvOKR.js +1 -0
- app/static/assets/cost-per-day-chart-YdqlkZVM.js +1 -0
- app/static/assets/createLucideIcon-DBLzLNh2.js +1 -0
- app/static/assets/dashboard-page-CSdhBy0f.js +2 -0
- app/static/assets/distribution-metric-format-yZTy1ZEu.js +1 -0
- app/static/assets/donut-chart-Bxc69cra.js +1 -0
- app/static/assets/download-kQteIcsB.js +1 -0
- app/static/assets/ellipsis-B_5OgIre.js +1 -0
- app/static/assets/empty-state-JcA7tc8m.js +1 -0
- app/static/assets/es2015-bMH8YKZT.js +41 -0
- app/static/assets/es6-B264ni6W.js +69 -0
- app/static/assets/external-link-UVAOgMQ0.js +1 -0
- app/static/assets/i18n-BWqZ9Ua2.js +1 -0
- app/static/assets/inbox-B_RM2s1J.js +1 -0
- app/static/assets/index-DIpd264Z.js +23 -0
- app/static/assets/index-DMeLKRJx.css +2 -0
- app/static/assets/inherit-badge-QhTYVaU6.js +1 -0
- app/static/assets/invite-dialog-0CYh0GiQ.js +1 -0
- app/static/assets/iso-Bpe5Ihbb.js +1 -0
- app/static/assets/label-CGaLmLNf.js +1 -0
- app/static/assets/lazy-recharts-Pv8p9ArM.js +2 -0
- app/static/assets/loading-overlay-CzKQfHLI.js +1 -0
- app/static/assets/mini-quota-bar-cGT_t8Ae.js +1 -0
- app/static/assets/model-distribution-donut-BKjAfT48.js +1 -0
- app/static/assets/model-source-multi-select-D2d06Vc7.js +1 -0
- app/static/assets/multi-select-filter-KIA5qU6Z.js +1 -0
- app/static/assets/oauth-dialog-XeaGGDjS.js +1 -0
- app/static/assets/pagination-controls-D1Ypd0RU.js +1 -0
- app/static/assets/payload-CIYnGZz9.js +1 -0
- app/static/assets/queue-wait-chart-VoV7Yg9L.js +1 -0
- app/static/assets/report-chart-card-BXDliwcc.js +1 -0
- app/static/assets/reports-page-CGCJeMHG.js +3 -0
- app/static/assets/reset-credit-confirm-dialog-QOifTnVB.js +1 -0
- app/static/assets/rolldown-runtime-hePW80VL.js +1 -0
- app/static/assets/search-HfhqwERt.js +1 -0
- app/static/assets/select-Cl33bz-g.js +1 -0
- app/static/assets/settings-page-mZj01HYl.js +2 -0
- app/static/assets/shim-CZwyM4Q7.js +1 -0
- app/static/assets/skeleton-C_EUc4Rp.js +1 -0
- app/static/assets/sparkline-chart-DdNNsddM.js +1 -0
- app/static/assets/status-badge-CHHA5X6c.js +1 -0
- app/static/assets/switch-CzxgokcM.js +1 -0
- app/static/assets/table-DXaNSQ0G.js +1 -0
- app/static/assets/time-to-first-token-chart--68L0u_v.js +1 -0
- app/static/assets/tokens-per-day-chart-Cs82BFuE.js +1 -0
- app/static/assets/tokens-per-second-chart-AD2dfOn6.js +1 -0
- app/static/assets/totp-settings-CRHtxQ7j.js +1 -0
- app/static/assets/trash-D3KyWr1h.js +1 -0
- app/static/assets/use-account-quota-display-BPmrTBTC.js +1 -0
- app/static/assets/use-accounts-PWlVGw3z.js +1 -0
- app/static/assets/use-auth-CuDlW3iC.js +1 -0
- app/static/assets/use-chart-colors-DIu6tClI.js +1 -0
- app/static/assets/use-model-sources-Bul24JBu.js +1 -0
- app/static/assets/use-models-DUCPUuZg.js +1 -0
- app/static/assets/use-reduced-motion-hAm5r6_d.js +1 -0
- app/static/assets/useTranslation--56nHhlH.js +1 -0
- app/static/assets/useragent-distribution-donut-Dup6sf_n.js +1 -0
- app/static/assets/users-uIC3TIlE.js +1 -0
- app/static/assets/vendor-query-_bHZuxEx.js +1 -0
- app/static/assets/vendor-react-BVL0YZPW.js +9 -0
- app/static/favicon.svg +10 -0
- app/static/fonts/GeistSans-Variable.woff2 +0 -0
- app/static/fonts/jetbrains-mono-latin-ext.woff2 +0 -0
- app/static/fonts/jetbrains-mono-latin.woff2 +0 -0
- app/static/index.html +29 -0
- codexlb_up-1.25.0b10.dist-info/METADATA +124 -0
- codexlb_up-1.25.0b10.dist-info/RECORD +827 -0
- codexlb_up-1.25.0b10.dist-info/WHEEL +4 -0
- codexlb_up-1.25.0b10.dist-info/entry_points.txt +3 -0
- codexlb_up-1.25.0b10.dist-info/licenses/LICENSE +21 -0
- config/additional_quota_registry.json +11 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"""The OIDC provider's connection settings: one sealed document, no environment.
|
|
2
|
+
|
|
3
|
+
Owner decision D6: an identity provider is connected from the dashboard, so
|
|
4
|
+
the issuer, the client credentials and the claim names live in
|
|
5
|
+
``dashboard_auth_providers.config_encrypted`` -- one JSON document sealed with
|
|
6
|
+
the product's single :class:`TokenEncryptor` -- and nowhere else. There is no
|
|
7
|
+
``Settings`` field, no ``.env.example`` line and no configuration tier for any
|
|
8
|
+
of it.
|
|
9
|
+
|
|
10
|
+
Everything an operator types that the server will later *fetch* is validated
|
|
11
|
+
here, once, at write time: the callback carries an authorization code and the
|
|
12
|
+
token request carries the client secret, so both legs must be ``https``, and a
|
|
13
|
+
URL aimed at the instance metadata endpoint or at a private-range address is
|
|
14
|
+
refused rather than fetched. This is a configuration-time check over the name
|
|
15
|
+
the operator wrote; it does not resolve DNS and so does not defend against
|
|
16
|
+
rebinding (see the change's design notes). It *does* normalise the name first,
|
|
17
|
+
because a check that refuses ``169.254.169.254`` and accepts ``2852039166`` --
|
|
18
|
+
the same address, spelled as one decimal integer, which is what the resolver
|
|
19
|
+
hands the socket -- is not a check at all. The redirect URI is validated the
|
|
20
|
+
same way and is the *only* source of the redirect URI the flow sends: nothing
|
|
21
|
+
in the stack validates or rewrites ``Host``, so a redirect URI reflected from
|
|
22
|
+
the request would be attacker-controllable.
|
|
23
|
+
|
|
24
|
+
:func:`validate_https_url` is also the gate for the endpoints the *discovery
|
|
25
|
+
document* advertises (``jwks_uri``, ``authorization_endpoint``,
|
|
26
|
+
``token_endpoint``), which is why the spelling gap mattered beyond the operator
|
|
27
|
+
who typed the issuer: the token endpoint is where the client secret is sent.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import hashlib
|
|
33
|
+
import ipaddress
|
|
34
|
+
import json
|
|
35
|
+
import socket
|
|
36
|
+
from dataclasses import dataclass
|
|
37
|
+
from typing import Final
|
|
38
|
+
from urllib.parse import SplitResult, urlsplit
|
|
39
|
+
|
|
40
|
+
from app.core.crypto import TokenEncryptor
|
|
41
|
+
from app.core.exceptions import DashboardValidationError
|
|
42
|
+
from app.db.models import DashboardAuthProvider
|
|
43
|
+
|
|
44
|
+
#: Where the identity provider sends the browser back. The stored redirect URI
|
|
45
|
+
#: must end here, so a provider can only be configured to return to this flow.
|
|
46
|
+
OIDC_CALLBACK_PATH: Final[str] = "/api/dashboard-auth/oidc/callback"
|
|
47
|
+
#: Where an operator's issuer publishes its metadata when they do not say.
|
|
48
|
+
OIDC_DISCOVERY_SUFFIX: Final[str] = "/.well-known/openid-configuration"
|
|
49
|
+
|
|
50
|
+
INVALID_PROVIDER_CONFIG_CODE: Final[str] = "invalid_provider_config"
|
|
51
|
+
CONFIG_NOT_SUPPORTED_CODE: Final[str] = "config_not_supported"
|
|
52
|
+
|
|
53
|
+
#: Host names that never name an identity provider and do name something we
|
|
54
|
+
#: must not fetch. ``metadata.google.internal`` is the cloud metadata endpoint;
|
|
55
|
+
#: ``localhost`` and anything under it resolve to the loopback interface.
|
|
56
|
+
_REFUSED_HOSTS: Final[frozenset[str]] = frozenset({"localhost", "metadata.google.internal"})
|
|
57
|
+
|
|
58
|
+
_MAX_URL_LENGTH: Final[int] = 512
|
|
59
|
+
_MAX_FIELD_LENGTH: Final[int] = 256
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class InvalidProviderConfigError(DashboardValidationError):
|
|
63
|
+
"""A connection field the server would have to fetch, or a missing one."""
|
|
64
|
+
|
|
65
|
+
code = INVALID_PROVIDER_CONFIG_CODE
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ConfigNotSupportedError(DashboardValidationError):
|
|
69
|
+
"""A connection document written to a row of a kind that has none.
|
|
70
|
+
|
|
71
|
+
The password row has no configuration at all and the trusted-header row's
|
|
72
|
+
is the deployment's own topology (two header names from the environment),
|
|
73
|
+
which the settings API reports and never accepts.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
code = CONFIG_NOT_SUPPORTED_CODE
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _refuse(field: str, reason: str) -> InvalidProviderConfigError:
|
|
80
|
+
return InvalidProviderConfigError(f"{field}: {reason}", param=field)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _split(value: str, *, field: str) -> SplitResult:
|
|
84
|
+
"""``urlsplit`` as a provider-configuration rule rather than a raw exception.
|
|
85
|
+
|
|
86
|
+
``urlsplit`` raises ``ValueError`` on a handful of shapes it cannot parse at
|
|
87
|
+
all -- an unterminated IPv6 literal (``https://[broken``) is the usual one.
|
|
88
|
+
Unparsed, that escapes this module entirely and answers ``500`` instead of
|
|
89
|
+
the field-named ``422`` every other malformed URL gets, so the one operator
|
|
90
|
+
typo that crashes the endpoint would be the one the error message could not
|
|
91
|
+
name.
|
|
92
|
+
|
|
93
|
+
``port`` is touched here for the same reason, and it has to be touched
|
|
94
|
+
deliberately: ``SplitResult.port`` is a lazy property, so ``:port``,
|
|
95
|
+
``:-1`` and ``:99999`` parse without complaint and raise only when somebody
|
|
96
|
+
reads them. Nothing in this module reads them, so an unchecked one would be
|
|
97
|
+
stored and then raise out of yarl at fetch time -- an operator typo
|
|
98
|
+
surfacing as "the identity provider could not be reached" instead of as the
|
|
99
|
+
field that is wrong.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
parts = urlsplit(value)
|
|
104
|
+
# Deliberately read and discarded: the parsing *is* the property.
|
|
105
|
+
_ = parts.port
|
|
106
|
+
except ValueError as exc:
|
|
107
|
+
raise _refuse(field, "is not a usable URL") from exc
|
|
108
|
+
return parts
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def validate_https_url(raw: str, *, field: str, allow_query: bool = True) -> str:
|
|
112
|
+
"""The one URL rule for every value this provider fetches or sends a browser to."""
|
|
113
|
+
|
|
114
|
+
value = raw.strip()
|
|
115
|
+
if not value:
|
|
116
|
+
raise _refuse(field, "is required")
|
|
117
|
+
if len(value) > _MAX_URL_LENGTH:
|
|
118
|
+
raise _refuse(field, f"must be at most {_MAX_URL_LENGTH} characters")
|
|
119
|
+
parts = _split(value, field=field)
|
|
120
|
+
if parts.scheme != "https":
|
|
121
|
+
raise _refuse(field, "must be an https URL")
|
|
122
|
+
if parts.username or parts.password:
|
|
123
|
+
raise _refuse(field, "must not carry a userinfo component")
|
|
124
|
+
if parts.fragment:
|
|
125
|
+
raise _refuse(field, "must not carry a fragment")
|
|
126
|
+
if not allow_query and parts.query:
|
|
127
|
+
raise _refuse(field, "must not carry a query string")
|
|
128
|
+
host = parts.hostname
|
|
129
|
+
if not host:
|
|
130
|
+
raise _refuse(field, "must name a host")
|
|
131
|
+
_assert_public_host(host, field=field)
|
|
132
|
+
return value
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _literal_address(name: str) -> ipaddress.IPv4Address | ipaddress.IPv6Address | None:
|
|
136
|
+
"""The address ``name`` *is*, in any spelling a resolver would accept, or ``None``.
|
|
137
|
+
|
|
138
|
+
``ipaddress`` only parses the canonical dotted quad, but ``getaddrinfo``
|
|
139
|
+
hands the socket an address for three more spellings of the same thing:
|
|
140
|
+
one decimal integer (``2130706433``), octal octets (``0177.0.0.1``) and
|
|
141
|
+
hexadecimal ones (``0x7f.0.0.1``) -- plus the short forms (``127.1``).
|
|
142
|
+
``inet_aton`` is exactly the parser the resolver reaches for first, so
|
|
143
|
+
asking it is asking the question the connection will answer. It resolves
|
|
144
|
+
nothing: a name it cannot parse is a name, not an address, and is left to
|
|
145
|
+
the rule above and to the DNS caveat in this module's docstring.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
return ipaddress.ip_address(name)
|
|
150
|
+
except ValueError:
|
|
151
|
+
pass
|
|
152
|
+
try:
|
|
153
|
+
return ipaddress.IPv4Address(socket.inet_aton(name))
|
|
154
|
+
except (OSError, ValueError):
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _assert_public_host(host: str, *, field: str) -> None:
|
|
159
|
+
name = host.rstrip(".").casefold()
|
|
160
|
+
if name in _REFUSED_HOSTS or name.endswith(".localhost"):
|
|
161
|
+
raise _refuse(field, "must not name the local host")
|
|
162
|
+
address = _literal_address(name)
|
|
163
|
+
if address is None:
|
|
164
|
+
return
|
|
165
|
+
if (
|
|
166
|
+
address.is_loopback
|
|
167
|
+
or address.is_link_local
|
|
168
|
+
or address.is_private
|
|
169
|
+
or address.is_reserved
|
|
170
|
+
or address.is_unspecified
|
|
171
|
+
):
|
|
172
|
+
raise _refuse(field, "must not name a loopback, link-local or private address")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def default_discovery_url(issuer: str) -> str:
|
|
176
|
+
return issuer.rstrip("/") + OIDC_DISCOVERY_SUFFIX
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@dataclass(frozen=True, slots=True)
|
|
180
|
+
class OidcProviderConfig:
|
|
181
|
+
"""What an operator connected, as the flow reads it.
|
|
182
|
+
|
|
183
|
+
``client_secret`` is in clear only inside this object and only between the
|
|
184
|
+
decrypt and the token request; no API returns it, no log line prints it and
|
|
185
|
+
no audit detail carries it.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
issuer: str
|
|
189
|
+
discovery_url: str
|
|
190
|
+
client_id: str
|
|
191
|
+
client_secret: str
|
|
192
|
+
redirect_uri: str
|
|
193
|
+
subject_claim: str = "sub"
|
|
194
|
+
email_claim: str = "email"
|
|
195
|
+
name_claim: str = "name"
|
|
196
|
+
groups_claim: str = "groups"
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _required_text(raw: object, *, field: str) -> str:
|
|
200
|
+
if not isinstance(raw, str) or not raw.strip():
|
|
201
|
+
raise _refuse(field, "is required")
|
|
202
|
+
value = raw.strip()
|
|
203
|
+
if len(value) > _MAX_FIELD_LENGTH:
|
|
204
|
+
raise _refuse(field, f"must be at most {_MAX_FIELD_LENGTH} characters")
|
|
205
|
+
return value
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _claim_name(raw: object, *, field: str, default: str) -> str:
|
|
209
|
+
if raw is None:
|
|
210
|
+
return default
|
|
211
|
+
value = _required_text(raw, field=field)
|
|
212
|
+
if any(character.isspace() for character in value):
|
|
213
|
+
raise _refuse(field, "must not contain whitespace")
|
|
214
|
+
return value
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def build_oidc_config(document: dict[str, object]) -> OidcProviderConfig:
|
|
218
|
+
"""Validate an operator-supplied document into the typed configuration.
|
|
219
|
+
|
|
220
|
+
Raises :class:`InvalidProviderConfigError` (422, naming the field) for
|
|
221
|
+
anything the flow would otherwise have to fetch or send blindly.
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
issuer = validate_https_url(
|
|
225
|
+
_required_text(document.get("issuer"), field="issuer"), field="issuer", allow_query=False
|
|
226
|
+
)
|
|
227
|
+
raw_discovery = document.get("discovery_url")
|
|
228
|
+
discovery_url = validate_https_url(
|
|
229
|
+
_required_text(raw_discovery, field="discovery_url")
|
|
230
|
+
if raw_discovery is not None
|
|
231
|
+
else default_discovery_url(issuer),
|
|
232
|
+
field="discovery_url",
|
|
233
|
+
)
|
|
234
|
+
redirect_uri = validate_https_url(
|
|
235
|
+
_required_text(document.get("redirect_uri"), field="redirect_uri"), field="redirect_uri", allow_query=False
|
|
236
|
+
)
|
|
237
|
+
if _split(redirect_uri, field="redirect_uri").path != OIDC_CALLBACK_PATH:
|
|
238
|
+
raise _refuse("redirect_uri", f"must end with {OIDC_CALLBACK_PATH}")
|
|
239
|
+
return OidcProviderConfig(
|
|
240
|
+
issuer=issuer,
|
|
241
|
+
discovery_url=discovery_url,
|
|
242
|
+
client_id=_required_text(document.get("client_id"), field="client_id"),
|
|
243
|
+
client_secret=_required_text(document.get("client_secret"), field="client_secret"),
|
|
244
|
+
redirect_uri=redirect_uri,
|
|
245
|
+
subject_claim=_claim_name(document.get("subject_claim"), field="subject_claim", default="sub"),
|
|
246
|
+
email_claim=_claim_name(document.get("email_claim"), field="email_claim", default="email"),
|
|
247
|
+
name_claim=_claim_name(document.get("name_claim"), field="name_claim", default="name"),
|
|
248
|
+
groups_claim=_claim_name(document.get("groups_claim"), field="groups_claim", default="groups"),
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _document(config: OidcProviderConfig) -> dict[str, str]:
|
|
253
|
+
"""The configuration as one ordered JSON-ready mapping. One writer, one reader."""
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
"issuer": config.issuer,
|
|
257
|
+
"discovery_url": config.discovery_url,
|
|
258
|
+
"client_id": config.client_id,
|
|
259
|
+
"client_secret": config.client_secret,
|
|
260
|
+
"redirect_uri": config.redirect_uri,
|
|
261
|
+
"subject_claim": config.subject_claim,
|
|
262
|
+
"email_claim": config.email_claim,
|
|
263
|
+
"name_claim": config.name_claim,
|
|
264
|
+
"groups_claim": config.groups_claim,
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def oidc_config_fingerprint(config: OidcProviderConfig) -> str:
|
|
269
|
+
"""A collision-free name for *this exact connection document*.
|
|
270
|
+
|
|
271
|
+
Two things need to tell one configuration from another and neither can use
|
|
272
|
+
the row's ``updated_at``, whose resolution is one second on SQLite: the
|
|
273
|
+
metadata cache (two writes inside the same second would otherwise share a
|
|
274
|
+
key and serve the previous issuer's endpoints to the new client secret) and
|
|
275
|
+
the test-login proof (a callback must only stamp the configuration it
|
|
276
|
+
actually verified). Both ask this function, so they cannot disagree about
|
|
277
|
+
what "the same configuration" means.
|
|
278
|
+
|
|
279
|
+
The digest is over the whole document including the secret, because a
|
|
280
|
+
rotated secret against an unchanged issuer is a different connection: the
|
|
281
|
+
cached token endpoint is fine, but the proof that the old secret worked
|
|
282
|
+
says nothing about the new one.
|
|
283
|
+
"""
|
|
284
|
+
|
|
285
|
+
return hashlib.sha256(json.dumps(_document(config), separators=(",", ":")).encode("utf-8")).hexdigest()
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def seal_oidc_config(config: OidcProviderConfig, encryptor: TokenEncryptor | None = None) -> bytes:
|
|
289
|
+
"""The document as it is stored: one sealed JSON blob, like the session cookie."""
|
|
290
|
+
|
|
291
|
+
return (encryptor or TokenEncryptor()).encrypt(json.dumps(_document(config), separators=(",", ":")))
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def load_oidc_config(
|
|
295
|
+
provider: DashboardAuthProvider, encryptor: TokenEncryptor | None = None
|
|
296
|
+
) -> OidcProviderConfig | None:
|
|
297
|
+
"""The stored configuration, or ``None`` when the row was never configured.
|
|
298
|
+
|
|
299
|
+
A blob that cannot be opened or does not validate is ``None`` too: an
|
|
300
|
+
unreadable configuration must refuse sign-ins, not raise out of a public
|
|
301
|
+
route where the difference would be observable.
|
|
302
|
+
"""
|
|
303
|
+
|
|
304
|
+
blob = provider.config_encrypted
|
|
305
|
+
if not blob:
|
|
306
|
+
return None
|
|
307
|
+
try:
|
|
308
|
+
document = json.loads((encryptor or TokenEncryptor()).decrypt(blob))
|
|
309
|
+
except Exception:
|
|
310
|
+
return None
|
|
311
|
+
if not isinstance(document, dict):
|
|
312
|
+
return None
|
|
313
|
+
try:
|
|
314
|
+
return build_oidc_config(document)
|
|
315
|
+
except InvalidProviderConfigError:
|
|
316
|
+
return None
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def mask_secret(secret: str) -> str:
|
|
320
|
+
"""``****`` plus the last four characters, so an operator can tell which secret is stored."""
|
|
321
|
+
|
|
322
|
+
return f"****{secret[-4:]}" if len(secret) > 4 else "****"
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def mask_oidc_config(config: OidcProviderConfig) -> dict[str, str]:
|
|
326
|
+
"""The settings API projection: everything in clear except the secret.
|
|
327
|
+
|
|
328
|
+
The client id is public by specification -- it rides in every authorization
|
|
329
|
+
URL the browser follows -- and the wizard has to show it, so masking it
|
|
330
|
+
would make the UI worse without hiding anything.
|
|
331
|
+
"""
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
"issuer": config.issuer,
|
|
335
|
+
"discoveryUrl": config.discovery_url,
|
|
336
|
+
"clientId": config.client_id,
|
|
337
|
+
"clientSecret": mask_secret(config.client_secret),
|
|
338
|
+
"redirectUri": config.redirect_uri,
|
|
339
|
+
"subjectClaim": config.subject_claim,
|
|
340
|
+
"emailClaim": config.email_claim,
|
|
341
|
+
"nameClaim": config.name_claim,
|
|
342
|
+
"groupsClaim": config.groups_claim,
|
|
343
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Any, cast
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import CursorResult, select, update
|
|
8
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
9
|
+
|
|
10
|
+
from app.db.models import DashboardAuthProvider, DashboardUser
|
|
11
|
+
from app.modules.dashboard_users.repository import DashboardUsersRepository
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AuthProvidersRepository:
|
|
15
|
+
def __init__(self, session: AsyncSession) -> None:
|
|
16
|
+
self._session = session
|
|
17
|
+
|
|
18
|
+
async def acquire_account_write_intent(self) -> None:
|
|
19
|
+
"""Serialise a provider enable against the account mutations its gate counts."""
|
|
20
|
+
|
|
21
|
+
await DashboardUsersRepository(self._session).acquire_write_intent()
|
|
22
|
+
|
|
23
|
+
async def count_qualifying_break_glass(self) -> int:
|
|
24
|
+
return await DashboardUsersRepository(self._session).count_qualifying_break_glass()
|
|
25
|
+
|
|
26
|
+
async def list_break_glass_designations(self) -> Sequence[DashboardUser]:
|
|
27
|
+
return await DashboardUsersRepository(self._session).list_break_glass_designations()
|
|
28
|
+
|
|
29
|
+
async def list_providers(self) -> Sequence[DashboardAuthProvider]:
|
|
30
|
+
stmt = select(DashboardAuthProvider).order_by(
|
|
31
|
+
DashboardAuthProvider.created_at.asc(), DashboardAuthProvider.id.asc()
|
|
32
|
+
)
|
|
33
|
+
return (await self._session.execute(stmt)).scalars().all()
|
|
34
|
+
|
|
35
|
+
async def get_provider(self, provider_id: str) -> DashboardAuthProvider | None:
|
|
36
|
+
return await self._session.get(DashboardAuthProvider, provider_id)
|
|
37
|
+
|
|
38
|
+
async def get_by_kind(self, kind: str, provider_key: str) -> DashboardAuthProvider | None:
|
|
39
|
+
"""The one row of a kind, whether or not it is enabled.
|
|
40
|
+
|
|
41
|
+
The pre-flight test sign-in needs exactly this: it runs *against* a row
|
|
42
|
+
that is still disabled, which is why it cannot go through the registry
|
|
43
|
+
(which only ever hands out active providers).
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
stmt = (
|
|
47
|
+
select(DashboardAuthProvider)
|
|
48
|
+
.where(DashboardAuthProvider.kind == kind)
|
|
49
|
+
.where(DashboardAuthProvider.provider_key == provider_key)
|
|
50
|
+
)
|
|
51
|
+
return (await self._session.execute(stmt)).scalar_one_or_none()
|
|
52
|
+
|
|
53
|
+
async def record_test_login(
|
|
54
|
+
self,
|
|
55
|
+
provider_id: str,
|
|
56
|
+
*,
|
|
57
|
+
user_id: str,
|
|
58
|
+
verified_at: datetime,
|
|
59
|
+
verified_config: bytes | None,
|
|
60
|
+
) -> bool:
|
|
61
|
+
"""Stamp a completed pre-flight sign-in on the row; ``False`` if it no longer applies.
|
|
62
|
+
|
|
63
|
+
One targeted ``UPDATE`` rather than a read-modify-write: the callback
|
|
64
|
+
that stamps this runs concurrently with whatever the settings page is
|
|
65
|
+
doing, and it has no business touching any other column.
|
|
66
|
+
|
|
67
|
+
``verified_config`` is the sealed document the round trip was actually
|
|
68
|
+
completed against, and the ``WHERE`` requires the row to still hold it.
|
|
69
|
+
Without that condition the stamp is not bound to anything: a
|
|
70
|
+
configuration write that commits while the callback is out at the
|
|
71
|
+
identity provider clears the proof, and this ``UPDATE`` would then
|
|
72
|
+
write it straight back against an issuer nobody has tested. The
|
|
73
|
+
ciphertext compares by identity rather than by value -- the sealing is
|
|
74
|
+
randomised -- which is exactly the conservative direction: a
|
|
75
|
+
re-save of a byte-identical document refuses the stamp instead of
|
|
76
|
+
accepting it.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
result = cast(
|
|
80
|
+
CursorResult[Any],
|
|
81
|
+
await self._session.execute(
|
|
82
|
+
update(DashboardAuthProvider)
|
|
83
|
+
.where(DashboardAuthProvider.id == provider_id)
|
|
84
|
+
.where(DashboardAuthProvider.config_encrypted == verified_config)
|
|
85
|
+
.values(test_login_user_id=user_id, test_login_verified_at=verified_at)
|
|
86
|
+
.execution_options(synchronize_session=False)
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
await self._session.commit()
|
|
90
|
+
return int(result.rowcount or 0) > 0
|
|
91
|
+
|
|
92
|
+
async def commit(self, provider: DashboardAuthProvider) -> DashboardAuthProvider:
|
|
93
|
+
"""Commit and reload the row (``expire_on_commit`` would otherwise lazy-load it outside the loop)."""
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
await self._session.commit()
|
|
97
|
+
except Exception:
|
|
98
|
+
await self._session.rollback()
|
|
99
|
+
raise
|
|
100
|
+
await self._session.refresh(provider)
|
|
101
|
+
return provider
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from pydantic import ConfigDict, Field
|
|
6
|
+
|
|
7
|
+
from app.modules.shared.schemas import DashboardModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AuthProviderResponse(DashboardModel):
|
|
11
|
+
"""A provider row as the settings UI sees it; secrets are masked, never returned."""
|
|
12
|
+
|
|
13
|
+
id: str
|
|
14
|
+
kind: str
|
|
15
|
+
provider_key: str
|
|
16
|
+
label: str
|
|
17
|
+
enabled: bool
|
|
18
|
+
#: ``enabled`` and admitted by the current ``dashboard_auth_mode``.
|
|
19
|
+
active: bool
|
|
20
|
+
unknown_identity_role_id: str | None = None
|
|
21
|
+
no_match_role_id: str | None = None
|
|
22
|
+
link_by_email: bool
|
|
23
|
+
skip_role_sync: bool
|
|
24
|
+
idp_mfa_enforced: bool
|
|
25
|
+
#: Masked provider configuration (``****last4`` per secret): the
|
|
26
|
+
#: trusted-header row's two header names, the OIDC row's connection
|
|
27
|
+
#: settings, empty for every other kind.
|
|
28
|
+
config: dict[str, str] = Field(default_factory=dict)
|
|
29
|
+
#: When an admin last completed a test sign-in against the stored
|
|
30
|
+
#: configuration. Enabling an OIDC row needs one no older than ten minutes,
|
|
31
|
+
#: and it belongs to the admin who ran it.
|
|
32
|
+
test_login_verified_at: datetime | None = None
|
|
33
|
+
created_at: datetime
|
|
34
|
+
updated_at: datetime
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class OidcConfigRequest(DashboardModel):
|
|
38
|
+
"""The OIDC connection settings, written whole.
|
|
39
|
+
|
|
40
|
+
A write replaces the stored document, so ``clientSecret`` is required every
|
|
41
|
+
time rather than inherited from whatever was there before: a partial write
|
|
42
|
+
that kept the old secret while repointing the issuer would send the
|
|
43
|
+
operator's credential to a server they had not yet typed it for. The
|
|
44
|
+
dashboard already holds every field in the connect wizard's form.
|
|
45
|
+
|
|
46
|
+
URLs are only length-checked here; the scheme, host and path rules live in
|
|
47
|
+
``auth_providers.config`` so the flow and the settings API cannot disagree
|
|
48
|
+
about what a usable issuer is.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
model_config = ConfigDict(extra="forbid")
|
|
52
|
+
|
|
53
|
+
issuer: str = Field(min_length=1, max_length=512)
|
|
54
|
+
#: Defaults to the issuer's ``/.well-known/openid-configuration``.
|
|
55
|
+
discovery_url: str | None = Field(default=None, max_length=512)
|
|
56
|
+
client_id: str = Field(min_length=1, max_length=256)
|
|
57
|
+
client_secret: str = Field(min_length=1, max_length=256)
|
|
58
|
+
#: Where the identity provider sends the browser back; must be this
|
|
59
|
+
#: install's own callback path, and is the only source of the redirect URI
|
|
60
|
+
#: the flow sends.
|
|
61
|
+
redirect_uri: str = Field(min_length=1, max_length=512)
|
|
62
|
+
subject_claim: str | None = Field(default=None, max_length=256)
|
|
63
|
+
email_claim: str | None = Field(default=None, max_length=256)
|
|
64
|
+
name_claim: str | None = Field(default=None, max_length=256)
|
|
65
|
+
groups_claim: str | None = Field(default=None, max_length=256)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class AuthProviderUpdateRequest(DashboardModel):
|
|
69
|
+
"""Fields left out are untouched; ``unknownIdentityRoleId: null`` means "refuse unknown identities"."""
|
|
70
|
+
|
|
71
|
+
model_config = ConfigDict(extra="forbid")
|
|
72
|
+
|
|
73
|
+
label: str | None = Field(default=None, min_length=1, max_length=64)
|
|
74
|
+
#: Turning a password-less sign-in method on needs a qualifying
|
|
75
|
+
#: break-glass account (409 ``break_glass_requires_totp``); turning one
|
|
76
|
+
#: off is never gated, so the recovery direction is always open.
|
|
77
|
+
enabled: bool | None = None
|
|
78
|
+
unknown_identity_role_id: str | None = None
|
|
79
|
+
no_match_role_id: str | None = None
|
|
80
|
+
link_by_email: bool | None = None
|
|
81
|
+
skip_role_sync: bool | None = None
|
|
82
|
+
idp_mfa_enforced: bool | None = None
|
|
83
|
+
#: The connection settings, on an ``oidc`` row only (422
|
|
84
|
+
#: ``config_not_supported`` elsewhere -- the trusted-header row's headers
|
|
85
|
+
#: are deployment topology and are read-only). Writing them clears any
|
|
86
|
+
#: test-login proof, so a proof obtained against one identity provider
|
|
87
|
+
#: cannot enable a different one.
|
|
88
|
+
config: OidcConfigRequest | None = None
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Idempotent seeding of the built-in ``dashboard_auth_providers`` rows.
|
|
2
|
+
|
|
3
|
+
The Alembic revision (with its revision-pinned table) and the test schema
|
|
4
|
+
reset (``create_all``) both call this so the password and trusted-header rows
|
|
5
|
+
exist wherever the table does. Rows are inserted with "do nothing on conflict"
|
|
6
|
+
semantics and never updated, so an operator's later PATCH survives restarts
|
|
7
|
+
and upgrades. The trusted-header row hands unknown identities the admin
|
|
8
|
+
preset (owner decision D10: an upgraded reverse-proxy install keeps admitting
|
|
9
|
+
everyone the proxy vouches for) and parks mapped users without a match on the
|
|
10
|
+
viewer preset.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import uuid
|
|
16
|
+
|
|
17
|
+
from sqlalchemy import Table, select
|
|
18
|
+
from sqlalchemy.dialects import postgresql, sqlite
|
|
19
|
+
from sqlalchemy.engine import Connection
|
|
20
|
+
from sqlalchemy.ext.asyncio import AsyncConnection
|
|
21
|
+
|
|
22
|
+
from app.core.auth.dashboard_access import PRESET_ROLE_IDS, PresetRoleSlug
|
|
23
|
+
from app.db.models import AuthProviderKind, Base, DashboardAuthProvider
|
|
24
|
+
|
|
25
|
+
_PROVIDERS_TABLE: Table = Base.metadata.tables[DashboardAuthProvider.__tablename__]
|
|
26
|
+
_PROVIDER_ID_NAMESPACE = uuid.UUID("2c8f6a4e-7d3b-4f1a-9e5c-1b0d8a7f3e21")
|
|
27
|
+
|
|
28
|
+
#: The one instance of a provider kind that has no per-instance configuration.
|
|
29
|
+
DEFAULT_PROVIDER_KEY = "default"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def auth_provider_id(kind: AuthProviderKind, provider_key: str = DEFAULT_PROVIDER_KEY) -> str:
|
|
33
|
+
"""Deterministic id of a built-in provider row (UUIDv5 of kind and key)."""
|
|
34
|
+
|
|
35
|
+
return str(uuid.uuid5(_PROVIDER_ID_NAMESPACE, f"codex-lb:auth-provider:{kind.value}:{provider_key}"))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def default_auth_provider_rows() -> list[dict[str, object]]:
|
|
39
|
+
return [
|
|
40
|
+
{
|
|
41
|
+
"id": auth_provider_id(AuthProviderKind.PASSWORD),
|
|
42
|
+
"kind": AuthProviderKind.PASSWORD.value,
|
|
43
|
+
"provider_key": DEFAULT_PROVIDER_KEY,
|
|
44
|
+
"enabled": True,
|
|
45
|
+
"label": "Password",
|
|
46
|
+
"unknown_identity_role_id": None,
|
|
47
|
+
"no_match_role_id": None,
|
|
48
|
+
"link_by_email": False,
|
|
49
|
+
"skip_role_sync": False,
|
|
50
|
+
"idp_mfa_enforced": False,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"id": auth_provider_id(AuthProviderKind.TRUSTED_HEADER),
|
|
54
|
+
"kind": AuthProviderKind.TRUSTED_HEADER.value,
|
|
55
|
+
"provider_key": DEFAULT_PROVIDER_KEY,
|
|
56
|
+
"enabled": True,
|
|
57
|
+
"label": "Reverse proxy",
|
|
58
|
+
"unknown_identity_role_id": PRESET_ROLE_IDS[PresetRoleSlug.ADMIN],
|
|
59
|
+
"no_match_role_id": PRESET_ROLE_IDS[PresetRoleSlug.VIEWER],
|
|
60
|
+
"link_by_email": False,
|
|
61
|
+
"skip_role_sync": False,
|
|
62
|
+
"idp_mfa_enforced": False,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
# Disabled, and with no role for an unmatched identity: an install
|
|
66
|
+
# that never connects an identity provider is unchanged, and one
|
|
67
|
+
# that does denies unknown identities until an operator says
|
|
68
|
+
# otherwise. The settings API has no create endpoint, so this row
|
|
69
|
+
# is what the connect wizard edits.
|
|
70
|
+
"id": auth_provider_id(AuthProviderKind.OIDC),
|
|
71
|
+
"kind": AuthProviderKind.OIDC.value,
|
|
72
|
+
"provider_key": DEFAULT_PROVIDER_KEY,
|
|
73
|
+
"enabled": False,
|
|
74
|
+
"label": "Single sign-on",
|
|
75
|
+
"unknown_identity_role_id": None,
|
|
76
|
+
"no_match_role_id": None,
|
|
77
|
+
"link_by_email": False,
|
|
78
|
+
"skip_role_sync": False,
|
|
79
|
+
"idp_mfa_enforced": False,
|
|
80
|
+
},
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _insert_ignore(connection: Connection, table: Table, rows: list[dict[str, object]]) -> None:
|
|
85
|
+
dialect = connection.dialect.name
|
|
86
|
+
if dialect == "postgresql":
|
|
87
|
+
connection.execute(postgresql.insert(table).values(rows).on_conflict_do_nothing(index_elements=["id"]))
|
|
88
|
+
return
|
|
89
|
+
if dialect == "sqlite":
|
|
90
|
+
connection.execute(sqlite.insert(table).values(rows).on_conflict_do_nothing(index_elements=["id"]))
|
|
91
|
+
return
|
|
92
|
+
existing = {row[0] for row in connection.execute(select(table.c.id)).all()}
|
|
93
|
+
missing = [row for row in rows if row["id"] not in existing]
|
|
94
|
+
if missing:
|
|
95
|
+
connection.execute(table.insert().values(missing))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def seed_default_auth_providers(connection: Connection, table: Table = _PROVIDERS_TABLE) -> None:
|
|
99
|
+
"""Insert any missing built-in provider row using a synchronous connection."""
|
|
100
|
+
|
|
101
|
+
_insert_ignore(connection, table, default_auth_provider_rows())
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
async def ensure_default_auth_providers(connection: AsyncConnection) -> None:
|
|
105
|
+
await connection.run_sync(seed_default_auth_providers)
|