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,156 @@
|
|
|
1
|
+
"""Issuing, rotating and revoking SCIM credentials (``security:write``).
|
|
2
|
+
|
|
3
|
+
Issue and rotate carry one gate the rest of the module does not:
|
|
4
|
+
``assert_can_delegate(principal.grants, ADMIN_GRANTS)``. The reasoning is
|
|
5
|
+
§4.6-A1's, applied to a fourth lever. A SCIM token lets a machine assert who
|
|
6
|
+
somebody is *and* attach a role, with no human in the loop per push, which is
|
|
7
|
+
the same class of lever as writing an OIDC connection or turning on e-mail
|
|
8
|
+
linking — and those are gated on admin-level delegation precisely because
|
|
9
|
+
``security:write`` is permission to administer sign-in, not permission to
|
|
10
|
+
become somebody else. Revoking is never gated: the narrowing direction stays
|
|
11
|
+
open everywhere in this plan.
|
|
12
|
+
|
|
13
|
+
The plaintext appears in the response to issue and to rotate and nowhere else —
|
|
14
|
+
not in a list, not in a later read, not in a log line, not in an audit row.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from fastapi import APIRouter, Depends, Request
|
|
20
|
+
|
|
21
|
+
from app.core.audit.service import AuditActor, AuditService, AuditTarget
|
|
22
|
+
from app.core.auth.dashboard_access import (
|
|
23
|
+
ADMIN_GRANTS,
|
|
24
|
+
DashboardPrincipal,
|
|
25
|
+
InsufficientDelegationError,
|
|
26
|
+
Permission,
|
|
27
|
+
assert_can_delegate,
|
|
28
|
+
)
|
|
29
|
+
from app.core.auth.dependencies import require_dashboard_permission, set_dashboard_error_format
|
|
30
|
+
from app.core.exceptions import DashboardNotFoundError, DashboardPermissionError
|
|
31
|
+
from app.db.models import DashboardScimToken
|
|
32
|
+
from app.dependencies import ScimTokensContext, get_scim_tokens_context
|
|
33
|
+
from app.modules.scim.api import SCIM_BASE_PATH
|
|
34
|
+
from app.modules.scim.repository import SCIM_PROVIDER
|
|
35
|
+
from app.modules.scim.schemas import (
|
|
36
|
+
ScimTokenCreateRequest,
|
|
37
|
+
ScimTokenIssuedResponse,
|
|
38
|
+
ScimTokenListResponse,
|
|
39
|
+
ScimTokenResponse,
|
|
40
|
+
)
|
|
41
|
+
from app.modules.scim.tokens import generate_token, hash_token, token_prefix
|
|
42
|
+
|
|
43
|
+
router = APIRouter(
|
|
44
|
+
prefix="/api/scim-tokens",
|
|
45
|
+
tags=["dashboard"],
|
|
46
|
+
dependencies=[
|
|
47
|
+
Depends(require_dashboard_permission(Permission.SECURITY_WRITE)),
|
|
48
|
+
Depends(set_dashboard_error_format),
|
|
49
|
+
],
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
#: Every token this release issues owns the install's single identity
|
|
53
|
+
#: namespace. The column is enforced on every lookup and insert anyway, because
|
|
54
|
+
#: deriving the namespace from the request instead is the bug the requirement
|
|
55
|
+
#: exists to prevent, and a scoping field that exists and is unenforced is
|
|
56
|
+
#: worse than none.
|
|
57
|
+
DEFAULT_PROVIDER_KEY = "default"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _response(row: DashboardScimToken) -> ScimTokenResponse:
|
|
61
|
+
return ScimTokenResponse(
|
|
62
|
+
id=row.id,
|
|
63
|
+
label=row.label,
|
|
64
|
+
token_prefix=row.token_prefix,
|
|
65
|
+
created_at=row.created_at,
|
|
66
|
+
created_by_user_id=row.created_by_user_id,
|
|
67
|
+
last_used_at=row.last_used_at,
|
|
68
|
+
rotated_at=row.rotated_at,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _require_admin_delegation(principal: DashboardPrincipal) -> None:
|
|
73
|
+
try:
|
|
74
|
+
assert_can_delegate(principal.grants, ADMIN_GRANTS)
|
|
75
|
+
except InsufficientDelegationError as exc:
|
|
76
|
+
raise DashboardPermissionError(
|
|
77
|
+
"Issuing an automatic-account-management credential needs the permissions it could hand out",
|
|
78
|
+
code="insufficient_delegation",
|
|
79
|
+
) from exc
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _audit(action: str, principal: DashboardPrincipal, row: DashboardScimToken, request: Request) -> None:
|
|
83
|
+
"""The row id and the label travel; the secret and its digest never do.
|
|
84
|
+
|
|
85
|
+
The audit sanitiser drops keys by exact name from a fixed set and knows
|
|
86
|
+
none of these, so it is not a safety net here — the rule is simply that no
|
|
87
|
+
detail dictionary ever carries the credential under any name.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
AuditService.log_async(
|
|
91
|
+
action,
|
|
92
|
+
actor_ip=request.client.host if request.client else None,
|
|
93
|
+
details={"label": row.label, "token_prefix": row.token_prefix, "provider": SCIM_PROVIDER},
|
|
94
|
+
actor=AuditActor.from_principal(principal),
|
|
95
|
+
target=AuditTarget("scim_token", row.id),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@router.get("", response_model=ScimTokenListResponse)
|
|
100
|
+
async def list_scim_tokens(
|
|
101
|
+
context: ScimTokensContext = Depends(get_scim_tokens_context),
|
|
102
|
+
) -> ScimTokenListResponse:
|
|
103
|
+
rows = await context.repository.list_tokens()
|
|
104
|
+
return ScimTokenListResponse(tokens=[_response(row) for row in rows], base_path=SCIM_BASE_PATH)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@router.post("", response_model=ScimTokenIssuedResponse, status_code=201)
|
|
108
|
+
async def issue_scim_token(
|
|
109
|
+
request: Request,
|
|
110
|
+
payload: ScimTokenCreateRequest,
|
|
111
|
+
context: ScimTokensContext = Depends(get_scim_tokens_context),
|
|
112
|
+
principal: DashboardPrincipal = Depends(require_dashboard_permission(Permission.SECURITY_WRITE)),
|
|
113
|
+
) -> ScimTokenIssuedResponse:
|
|
114
|
+
_require_admin_delegation(principal)
|
|
115
|
+
secret = generate_token()
|
|
116
|
+
row = await context.repository.create(
|
|
117
|
+
label=payload.label,
|
|
118
|
+
token_hash=hash_token(secret),
|
|
119
|
+
token_prefix=token_prefix(secret),
|
|
120
|
+
provider_key=DEFAULT_PROVIDER_KEY,
|
|
121
|
+
created_by_user_id=principal.user_id,
|
|
122
|
+
)
|
|
123
|
+
_audit("scim_token_issued", principal, row, request)
|
|
124
|
+
return ScimTokenIssuedResponse(token=_response(row), secret=secret)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@router.post("/{token_id}/rotate", response_model=ScimTokenIssuedResponse)
|
|
128
|
+
async def rotate_scim_token(
|
|
129
|
+
token_id: str,
|
|
130
|
+
request: Request,
|
|
131
|
+
context: ScimTokensContext = Depends(get_scim_tokens_context),
|
|
132
|
+
principal: DashboardPrincipal = Depends(require_dashboard_permission(Permission.SECURITY_WRITE)),
|
|
133
|
+
) -> ScimTokenIssuedResponse:
|
|
134
|
+
_require_admin_delegation(principal)
|
|
135
|
+
secret = generate_token()
|
|
136
|
+
# In place on the same row: the id, the label and the sync history survive,
|
|
137
|
+
# and the previous secret stops working the moment this commits, because
|
|
138
|
+
# the row is the only verifier there is.
|
|
139
|
+
row = await context.repository.rotate(token_id, token_hash=hash_token(secret), token_prefix=token_prefix(secret))
|
|
140
|
+
if row is None:
|
|
141
|
+
raise DashboardNotFoundError("No such token", code="scim_token_not_found")
|
|
142
|
+
_audit("scim_token_rotated", principal, row, request)
|
|
143
|
+
return ScimTokenIssuedResponse(token=_response(row), secret=secret)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@router.delete("/{token_id}", status_code=204)
|
|
147
|
+
async def revoke_scim_token(
|
|
148
|
+
token_id: str,
|
|
149
|
+
request: Request,
|
|
150
|
+
context: ScimTokensContext = Depends(get_scim_tokens_context),
|
|
151
|
+
principal: DashboardPrincipal = Depends(require_dashboard_permission(Permission.SECURITY_WRITE)),
|
|
152
|
+
) -> None:
|
|
153
|
+
row = await context.repository.get(token_id)
|
|
154
|
+
if row is None or not await context.repository.delete(token_id):
|
|
155
|
+
raise DashboardNotFoundError("No such token", code="scim_token_not_found")
|
|
156
|
+
_audit("scim_token_revoked", principal, row, request)
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Queries and writes for SCIM bearer tokens and the accounts they provision.
|
|
2
|
+
|
|
3
|
+
Two concerns live here because they share one rule: every read and every write
|
|
4
|
+
is bound to the ``provider_key`` carried on the token row. An account the token
|
|
5
|
+
did not provision has no ``scim`` identity in that namespace and is therefore
|
|
6
|
+
not addressable at all — which is what stands in for ``assert_can_act_on`` on a
|
|
7
|
+
surface that has no principal.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import uuid
|
|
13
|
+
from collections.abc import Sequence
|
|
14
|
+
from datetime import UTC, datetime
|
|
15
|
+
|
|
16
|
+
from sqlalchemy import delete, func, or_, select, update
|
|
17
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
18
|
+
from sqlalchemy.orm import selectinload
|
|
19
|
+
|
|
20
|
+
from app.db.models import (
|
|
21
|
+
AuditLog,
|
|
22
|
+
DashboardIdentity,
|
|
23
|
+
DashboardRoleRecord,
|
|
24
|
+
DashboardScimToken,
|
|
25
|
+
DashboardUser,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
#: The provider string on every identity row this surface writes.
|
|
29
|
+
SCIM_PROVIDER = "scim"
|
|
30
|
+
#: How many resources one list page may carry, whatever the caller asks for.
|
|
31
|
+
#: RFC 7644 §3.4.2.4 lets a server return fewer than ``count``; clamping is the
|
|
32
|
+
#: bound on a read, the way the body cap is the bound on a write.
|
|
33
|
+
MAX_LIST_COUNT = 200
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _identity_query():
|
|
37
|
+
return select(DashboardIdentity).options(
|
|
38
|
+
selectinload(DashboardIdentity.user).selectinload(DashboardUser.role).selectinload(DashboardRoleRecord.grants)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ScimTokensRepository:
|
|
43
|
+
"""The credential rows. Nothing here ever returns or accepts a plaintext token."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, session: AsyncSession) -> None:
|
|
46
|
+
self._session = session
|
|
47
|
+
|
|
48
|
+
async def get_by_hash(self, token_hash: str) -> DashboardScimToken | None:
|
|
49
|
+
stmt = select(DashboardScimToken).where(DashboardScimToken.token_hash == token_hash)
|
|
50
|
+
return (await self._session.execute(stmt)).scalar_one_or_none()
|
|
51
|
+
|
|
52
|
+
async def get(self, token_id: str) -> DashboardScimToken | None:
|
|
53
|
+
return await self._session.get(DashboardScimToken, token_id)
|
|
54
|
+
|
|
55
|
+
async def list_tokens(self) -> Sequence[DashboardScimToken]:
|
|
56
|
+
stmt = select(DashboardScimToken).order_by(DashboardScimToken.created_at.asc(), DashboardScimToken.id.asc())
|
|
57
|
+
return (await self._session.execute(stmt)).scalars().all()
|
|
58
|
+
|
|
59
|
+
async def count_tokens(self) -> int:
|
|
60
|
+
return int((await self._session.execute(select(func.count()).select_from(DashboardScimToken))).scalar_one())
|
|
61
|
+
|
|
62
|
+
async def create(
|
|
63
|
+
self,
|
|
64
|
+
*,
|
|
65
|
+
label: str,
|
|
66
|
+
token_hash: str,
|
|
67
|
+
token_prefix: str,
|
|
68
|
+
provider_key: str,
|
|
69
|
+
created_by_user_id: str | None,
|
|
70
|
+
) -> DashboardScimToken:
|
|
71
|
+
row = DashboardScimToken(
|
|
72
|
+
id=str(uuid.uuid4()),
|
|
73
|
+
label=label,
|
|
74
|
+
token_hash=token_hash,
|
|
75
|
+
token_prefix=token_prefix,
|
|
76
|
+
provider_key=provider_key,
|
|
77
|
+
created_by_user_id=created_by_user_id,
|
|
78
|
+
)
|
|
79
|
+
self._session.add(row)
|
|
80
|
+
await self._session.commit()
|
|
81
|
+
await self._session.refresh(row)
|
|
82
|
+
return row
|
|
83
|
+
|
|
84
|
+
async def rotate(self, token_id: str, *, token_hash: str, token_prefix: str) -> DashboardScimToken | None:
|
|
85
|
+
"""Replace the verifier on the same row; the id, label and sync history survive."""
|
|
86
|
+
|
|
87
|
+
updated = await self._session.execute(
|
|
88
|
+
update(DashboardScimToken)
|
|
89
|
+
.where(DashboardScimToken.id == token_id)
|
|
90
|
+
.values(token_hash=token_hash, token_prefix=token_prefix, rotated_at=datetime.now(UTC))
|
|
91
|
+
.returning(DashboardScimToken.id)
|
|
92
|
+
.execution_options(synchronize_session=False)
|
|
93
|
+
)
|
|
94
|
+
if updated.scalar_one_or_none() is None:
|
|
95
|
+
await self._session.rollback()
|
|
96
|
+
return None
|
|
97
|
+
await self._session.commit()
|
|
98
|
+
self._session.expire_all()
|
|
99
|
+
return await self.get(token_id)
|
|
100
|
+
|
|
101
|
+
async def delete(self, token_id: str) -> bool:
|
|
102
|
+
deleted = await self._session.execute(
|
|
103
|
+
delete(DashboardScimToken)
|
|
104
|
+
.where(DashboardScimToken.id == token_id)
|
|
105
|
+
.returning(DashboardScimToken.id)
|
|
106
|
+
.execution_options(synchronize_session=False)
|
|
107
|
+
)
|
|
108
|
+
if deleted.scalar_one_or_none() is None:
|
|
109
|
+
await self._session.rollback()
|
|
110
|
+
return False
|
|
111
|
+
await self._session.commit()
|
|
112
|
+
return True
|
|
113
|
+
|
|
114
|
+
async def record_used(self, token_id: str, now: datetime) -> None:
|
|
115
|
+
"""Advance ``last_used_at`` monotonically, so replicas may write out of order.
|
|
116
|
+
|
|
117
|
+
The guard is the whole point: two replicas serving two pushes can
|
|
118
|
+
commit in either order, and a blind ``SET`` would let the older one win
|
|
119
|
+
and make the card's "last sync" go backwards.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
await self._session.execute(
|
|
123
|
+
update(DashboardScimToken)
|
|
124
|
+
.where(DashboardScimToken.id == token_id)
|
|
125
|
+
.where(or_(DashboardScimToken.last_used_at.is_(None), DashboardScimToken.last_used_at < now))
|
|
126
|
+
.values(last_used_at=now)
|
|
127
|
+
.execution_options(synchronize_session=False)
|
|
128
|
+
)
|
|
129
|
+
await self._session.commit()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class ScimUsersRepository:
|
|
133
|
+
"""Accounts as this surface sees them: through their ``scim`` identity row."""
|
|
134
|
+
|
|
135
|
+
def __init__(self, session: AsyncSession) -> None:
|
|
136
|
+
self._session = session
|
|
137
|
+
|
|
138
|
+
async def get_identity_by_subject(self, provider_key: str, subject: str) -> DashboardIdentity | None:
|
|
139
|
+
"""The identity a push names. ``subject`` is compared exactly: it is opaque."""
|
|
140
|
+
|
|
141
|
+
stmt = (
|
|
142
|
+
_identity_query()
|
|
143
|
+
.where(DashboardIdentity.provider == SCIM_PROVIDER)
|
|
144
|
+
.where(DashboardIdentity.provider_key == provider_key)
|
|
145
|
+
.where(DashboardIdentity.subject == subject)
|
|
146
|
+
)
|
|
147
|
+
return (await self._session.execute(stmt)).scalar_one_or_none()
|
|
148
|
+
|
|
149
|
+
async def get_identity_by_user(self, provider_key: str, user_id: str) -> DashboardIdentity | None:
|
|
150
|
+
"""The resource a SCIM ``id`` names, or ``None`` when it is outside this namespace.
|
|
151
|
+
|
|
152
|
+
An account without a ``scim`` identity here is not a resource of this
|
|
153
|
+
token, whatever else it is on the install, and the route answers ``404``
|
|
154
|
+
rather than distinguishing "no such account" from "not yours".
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
stmt = (
|
|
158
|
+
_identity_query()
|
|
159
|
+
.where(DashboardIdentity.provider == SCIM_PROVIDER)
|
|
160
|
+
.where(DashboardIdentity.provider_key == provider_key)
|
|
161
|
+
.where(DashboardIdentity.user_id == user_id)
|
|
162
|
+
)
|
|
163
|
+
return (await self._session.execute(stmt)).scalars().first()
|
|
164
|
+
|
|
165
|
+
async def audit_row_exists(self, action: str, user_id: str) -> bool:
|
|
166
|
+
"""Whether this surface has already recorded ``action`` against that account.
|
|
167
|
+
|
|
168
|
+
The audit row is the marker for the decisions this surface makes once
|
|
169
|
+
per account rather than once per push — an identity provider re-pushes
|
|
170
|
+
its whole directory on a schedule, and a row per push buries the one an
|
|
171
|
+
operator needs. ``idx_audit_logs_target`` covers the predicate.
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
stmt = (
|
|
175
|
+
select(AuditLog.id)
|
|
176
|
+
.where(AuditLog.action == action)
|
|
177
|
+
.where(AuditLog.target_type == "user")
|
|
178
|
+
.where(AuditLog.target_id == user_id)
|
|
179
|
+
.limit(1)
|
|
180
|
+
)
|
|
181
|
+
return (await self._session.execute(stmt)).scalars().first() is not None
|
|
182
|
+
|
|
183
|
+
async def list_identities(
|
|
184
|
+
self, provider_key: str, *, user_name: str | None, offset: int, limit: int
|
|
185
|
+
) -> tuple[Sequence[DashboardIdentity], int]:
|
|
186
|
+
"""One page of this namespace's resources plus the unpaged total.
|
|
187
|
+
|
|
188
|
+
Disabled and invited accounts are included on purpose: a deprovisioned
|
|
189
|
+
person has to stay rediscoverable by the filter that found them.
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
base = (
|
|
193
|
+
select(DashboardIdentity.id)
|
|
194
|
+
.where(DashboardIdentity.provider == SCIM_PROVIDER)
|
|
195
|
+
.where(DashboardIdentity.provider_key == provider_key)
|
|
196
|
+
)
|
|
197
|
+
if user_name is not None:
|
|
198
|
+
base = base.where(func.lower(DashboardIdentity.user_name) == user_name.casefold())
|
|
199
|
+
total = int((await self._session.execute(select(func.count()).select_from(base.subquery()))).scalar_one())
|
|
200
|
+
page = _identity_query().where(DashboardIdentity.id.in_(base))
|
|
201
|
+
rows = (
|
|
202
|
+
(
|
|
203
|
+
await self._session.execute(
|
|
204
|
+
page.order_by(DashboardIdentity.created_at.asc(), DashboardIdentity.id.asc())
|
|
205
|
+
.offset(offset)
|
|
206
|
+
.limit(limit)
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
.scalars()
|
|
210
|
+
.all()
|
|
211
|
+
)
|
|
212
|
+
return rows, total
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""The SCIM 2.0 wire shapes this surface reads and writes (RFC 7643 / RFC 7644).
|
|
2
|
+
|
|
3
|
+
Attributes this product does not model are ignored rather than refused. Every
|
|
4
|
+
identity provider sends more than a core user — ``meta``, ``title``,
|
|
5
|
+
``locale``, phone numbers, an enterprise extension — and refusing those would
|
|
6
|
+
turn a routine Okta or Entra push into a ``400`` while protecting nothing: the
|
|
7
|
+
bound on a request is the declared body size (checked before the body is read)
|
|
8
|
+
plus the length and cardinality caps below, not the attribute vocabulary.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
17
|
+
|
|
18
|
+
from app.modules.shared.schemas import DashboardModel
|
|
19
|
+
|
|
20
|
+
USER_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:User"
|
|
21
|
+
LIST_RESPONSE_SCHEMA = "urn:ietf:params:scim:api:messages:2.0:ListResponse"
|
|
22
|
+
PATCH_OP_SCHEMA = "urn:ietf:params:scim:api:messages:2.0:PatchOp"
|
|
23
|
+
|
|
24
|
+
#: 64 KiB. A core user resource is a few hundred bytes and the largest patch
|
|
25
|
+
#: this surface accepts is a few kilobytes; the global ingress budget of 32 MiB
|
|
26
|
+
#: is not a bound for this endpoint in any useful sense.
|
|
27
|
+
MAX_BODY_BYTES = 64 * 1024
|
|
28
|
+
#: Cardinality caps. A multi-valued attribute an identity provider fills with
|
|
29
|
+
#: thousands of entries is a denial of service, not a user.
|
|
30
|
+
MAX_MULTI_VALUED = 8
|
|
31
|
+
MAX_PATCH_OPERATIONS = 32
|
|
32
|
+
|
|
33
|
+
#: ``active`` arrives as a JSON boolean from most identity providers and as the
|
|
34
|
+
#: strings ``"True"``/``"False"`` from Entra ID's SCIM client. Both mean the
|
|
35
|
+
#: same thing, and guessing wrong deprovisions the wrong people.
|
|
36
|
+
_TRUE_STRINGS = frozenset({"true", "1"})
|
|
37
|
+
_FALSE_STRINGS = frozenset({"false", "0"})
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def coerce_active(value: Any) -> bool | None:
|
|
41
|
+
"""A SCIM ``active`` value as a boolean, or ``None`` when it is not one."""
|
|
42
|
+
|
|
43
|
+
if isinstance(value, bool):
|
|
44
|
+
return value
|
|
45
|
+
if isinstance(value, str):
|
|
46
|
+
folded = value.strip().casefold()
|
|
47
|
+
if folded in _TRUE_STRINGS:
|
|
48
|
+
return True
|
|
49
|
+
if folded in _FALSE_STRINGS:
|
|
50
|
+
return False
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ScimModel(BaseModel):
|
|
55
|
+
model_config = ConfigDict(extra="ignore", populate_by_name=True)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ScimMultiValue(ScimModel):
|
|
59
|
+
value: str | None = Field(default=None, max_length=320)
|
|
60
|
+
type: str | None = Field(default=None, max_length=32)
|
|
61
|
+
primary: bool = False
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ScimName(ScimModel):
|
|
65
|
+
formatted: str | None = Field(default=None, max_length=128)
|
|
66
|
+
given_name: str | None = Field(default=None, alias="givenName", max_length=64)
|
|
67
|
+
family_name: str | None = Field(default=None, alias="familyName", max_length=64)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ScimUserRequest(ScimModel):
|
|
71
|
+
"""A ``POST`` or ``PUT`` body.
|
|
72
|
+
|
|
73
|
+
Every attribute is optional at the model level; which of them a given
|
|
74
|
+
operation requires is the service's rule, so the refusal names the
|
|
75
|
+
attribute in RFC 7644's envelope instead of FastAPI's.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
schemas: list[str] = Field(default_factory=list, max_length=MAX_MULTI_VALUED)
|
|
79
|
+
user_name: str | None = Field(default=None, alias="userName", max_length=256)
|
|
80
|
+
external_id: str | None = Field(default=None, alias="externalId", max_length=512)
|
|
81
|
+
display_name: str | None = Field(default=None, alias="displayName", max_length=128)
|
|
82
|
+
active: Any = None
|
|
83
|
+
name: ScimName | None = None
|
|
84
|
+
emails: list[ScimMultiValue] = Field(default_factory=list, max_length=MAX_MULTI_VALUED)
|
|
85
|
+
roles: list[ScimMultiValue] = Field(default_factory=list, max_length=MAX_MULTI_VALUED)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
#: A patch ``path`` is looked up and, when this resource does not model it,
|
|
89
|
+
#: dropped — so its length bounds nothing that gets stored, and a cap tight
|
|
90
|
+
#: enough to refuse a real one only turns a routine push into a ``400``. The
|
|
91
|
+
#: schema-qualified spellings identity providers send are long:
|
|
92
|
+
#: ``urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager.displayName``
|
|
93
|
+
#: is 78 characters, and vendors mint longer custom schema URNs. This matches
|
|
94
|
+
#: ``externalId``'s cap, which is the widest opaque string on the resource;
|
|
95
|
+
#: with ``MAX_PATCH_OPERATIONS`` it stays an order of magnitude inside the body
|
|
96
|
+
#: bound, which is the bound that actually protects this endpoint.
|
|
97
|
+
MAX_PATCH_PATH_LENGTH = 512
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class ScimPatchOperation(ScimModel):
|
|
101
|
+
op: str = Field(max_length=16)
|
|
102
|
+
path: str | None = Field(default=None, max_length=MAX_PATCH_PATH_LENGTH)
|
|
103
|
+
value: Any = None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class ScimPatchRequest(ScimModel):
|
|
107
|
+
schemas: list[str] = Field(default_factory=list, max_length=MAX_MULTI_VALUED)
|
|
108
|
+
operations: list[ScimPatchOperation] = Field(
|
|
109
|
+
default_factory=list, alias="Operations", max_length=MAX_PATCH_OPERATIONS
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class ScimTokenResponse(DashboardModel):
|
|
114
|
+
"""A token row as the dashboard sees it: never the secret, only its head."""
|
|
115
|
+
|
|
116
|
+
id: str
|
|
117
|
+
label: str
|
|
118
|
+
token_prefix: str
|
|
119
|
+
created_at: datetime
|
|
120
|
+
created_by_user_id: str | None = None
|
|
121
|
+
last_used_at: datetime | None = None
|
|
122
|
+
rotated_at: datetime | None = None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class ScimTokenIssuedResponse(DashboardModel):
|
|
126
|
+
"""The one and only response that carries a plaintext token."""
|
|
127
|
+
|
|
128
|
+
token: ScimTokenResponse
|
|
129
|
+
#: Shown once. Never listed, never logged, never audited, never returned again.
|
|
130
|
+
secret: str
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class ScimTokenListResponse(DashboardModel):
|
|
134
|
+
tokens: list[ScimTokenResponse]
|
|
135
|
+
#: Where the identity provider points its SCIM connector.
|
|
136
|
+
base_path: str
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class ScimTokenCreateRequest(DashboardModel):
|
|
140
|
+
#: ``str_strip_whitespace`` so the value the length bounds are checked
|
|
141
|
+
#: against is the value that gets stored: trimming after validation would
|
|
142
|
+
#: let ``" "`` past ``min_length`` and store an unnamed credential.
|
|
143
|
+
model_config = ConfigDict(extra="forbid", populate_by_name=True, str_strip_whitespace=True)
|
|
144
|
+
|
|
145
|
+
label: str = Field(min_length=1, max_length=64)
|