imbi-api 2.11.6__tar.gz → 2.12.2__tar.gz
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.
- {imbi_api-2.11.6 → imbi_api-2.12.2}/PKG-INFO +2 -9
- imbi_api-2.12.2/docs/adr/0016-delegated-execution-for-scheduled-jobs.md +261 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/docs/adr.md +31 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/pyproject.toml +3 -11
- imbi_api-2.12.2/src/imbi_api/auth/login_providers.py +205 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/auth/models.py +2 -2
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/auth/oauth.py +8 -16
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/auth/permissions.py +3 -5
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/auth/seed.py +88 -17
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/backfill_embeddings.py +1 -1
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/commit_sync/service.py +57 -125
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/domain/models.py +178 -319
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/__init__.py +4 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/_helpers.py +25 -22
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/_json_fields.py +1 -1
- imbi_api-2.12.2/src/imbi_api/endpoints/admin_plugins.py +156 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/auth.py +50 -452
- imbi_api-2.12.2/src/imbi_api/endpoints/auth_providers.py +335 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/blueprints.py +1 -1
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/comments.py +1 -1
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/events.py +5 -5
- imbi_api-2.12.2/src/imbi_api/endpoints/integrations.py +884 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/organizations.py +3 -23
- imbi_api-2.12.2/src/imbi_api/endpoints/plugins.py +45 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/project_analysis.py +47 -55
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/project_commit_sync.py +14 -12
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/project_configuration.py +62 -89
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/project_deployments.py +31 -34
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/project_incidents.py +15 -17
- imbi_api-2.12.2/src/imbi_api/endpoints/project_integrations.py +271 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/project_logs.py +48 -45
- imbi_api-2.12.2/src/imbi_api/endpoints/project_plugins.py +142 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/project_pr_sync.py +12 -9
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/projects.py +41 -37
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/pull_requests.py +10 -6
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/search.py +1 -1
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/user_activity.py +2 -2
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/users.py +6 -5
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/webhooks.py +44 -44
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/entrypoint.py +6 -49
- imbi_api-2.12.2/src/imbi_api/identity/attribution.py +112 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/identity/endpoints.py +54 -39
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/identity/errors.py +9 -7
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/identity/flows.py +152 -267
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/identity/host_integration.py +66 -42
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/identity/models.py +11 -9
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/identity/repository.py +126 -48
- imbi_api-2.12.2/src/imbi_api/identity/resolution.py +237 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/identity/state.py +6 -6
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/identity/sweeper.py +20 -19
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/models.py +13 -4
- imbi_api-2.12.2/src/imbi_api/plugins/assignment_writer.py +125 -0
- imbi_api-2.12.2/src/imbi_api/plugins/assignments.py +255 -0
- imbi_api-2.12.2/src/imbi_api/plugins/credentials.py +184 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/plugins/lifecycle.py +3 -3
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/plugins/lifecycle_dispatch.py +47 -44
- imbi_api-2.12.2/src/imbi_api/plugins/resolution.py +225 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/pr_sync/service.py +60 -125
- imbi_api-2.12.2/tests/auth/test_login_providers.py +291 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/auth/test_oauth.py +8 -5
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/auth/test_seed.py +41 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_admin_plugins.py +31 -31
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_auth.py +91 -701
- imbi_api-2.12.2/tests/endpoints/test_auth_providers.py +304 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_events.py +6 -8
- imbi_api-2.12.2/tests/endpoints/test_integrations.py +709 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_plugin_label_validation.py +31 -27
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_plugins.py +41 -38
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_project_analysis.py +57 -48
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_project_commit_sync.py +5 -5
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_project_configuration.py +69 -55
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_project_deployments.py +102 -134
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_project_incidents.py +67 -49
- imbi_api-2.12.2/tests/endpoints/test_project_integrations.py +179 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_project_lifecycle.py +7 -5
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_project_logs.py +71 -60
- imbi_api-2.12.2/tests/endpoints/test_project_plugins.py +143 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_project_response_surfacing.py +2 -2
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_projects.py +38 -27
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_releases.py +3 -2
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_service_writeback.py +13 -9
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_users.py +8 -8
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_webhooks.py +42 -40
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/identity/test_endpoints.py +40 -6
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/identity/test_errors.py +4 -4
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/identity/test_flows.py +211 -149
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/identity/test_host_integration.py +89 -9
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/identity/test_repository.py +13 -12
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/identity/test_resolution.py +173 -97
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/identity/test_state.py +14 -14
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/identity/test_sweeper.py +13 -13
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/support.py +31 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_assignment_writer.py +30 -27
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_commit_sync_service.py +53 -77
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_entrypoint.py +17 -44
- imbi_api-2.12.2/tests/test_identity_attribution.py +128 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_lifecycle_dispatch.py +119 -161
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_models.py +12 -190
- imbi_api-2.12.2/tests/test_plugins.py +942 -0
- imbi_api-2.12.2/tests/test_plugins_assignments.py +186 -0
- imbi_api-2.12.2/tests/test_plugins_credentials.py +183 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_pr_sync_service.py +53 -77
- {imbi_api-2.11.6 → imbi_api-2.12.2}/uv.lock +14 -120
- imbi_api-2.11.6/CODE_REVIEW_PUNCHLIST.md +0 -172
- imbi_api-2.11.6/src/imbi_api/auth/login_providers.py +0 -268
- imbi_api-2.11.6/src/imbi_api/endpoints/admin_plugins.py +0 -502
- imbi_api-2.11.6/src/imbi_api/endpoints/auth_providers.py +0 -673
- imbi_api-2.11.6/src/imbi_api/endpoints/identity_plugins.py +0 -79
- imbi_api-2.11.6/src/imbi_api/endpoints/plugins.py +0 -65
- imbi_api-2.11.6/src/imbi_api/endpoints/project_plugins.py +0 -180
- imbi_api-2.11.6/src/imbi_api/endpoints/project_type_plugins.py +0 -140
- imbi_api-2.11.6/src/imbi_api/endpoints/service_plugins.py +0 -922
- imbi_api-2.11.6/src/imbi_api/endpoints/third_party_services.py +0 -1691
- imbi_api-2.11.6/src/imbi_api/identity/attribution.py +0 -130
- imbi_api-2.11.6/src/imbi_api/identity/resolution.py +0 -203
- imbi_api-2.11.6/src/imbi_api/plugins/assignment_writer.py +0 -140
- imbi_api-2.11.6/src/imbi_api/plugins/assignments.py +0 -168
- imbi_api-2.11.6/src/imbi_api/plugins/credentials.py +0 -363
- imbi_api-2.11.6/src/imbi_api/plugins/resolution.py +0 -706
- imbi_api-2.11.6/tests/auth/test_login_providers.py +0 -282
- imbi_api-2.11.6/tests/endpoints/test_auth_providers.py +0 -621
- imbi_api-2.11.6/tests/endpoints/test_project_plugins.py +0 -193
- imbi_api-2.11.6/tests/endpoints/test_project_type_plugins.py +0 -151
- imbi_api-2.11.6/tests/endpoints/test_service_plugins.py +0 -1191
- imbi_api-2.11.6/tests/endpoints/test_third_party_services.py +0 -2153
- imbi_api-2.11.6/tests/test_identity_attribution.py +0 -158
- imbi_api-2.11.6/tests/test_plugins.py +0 -1982
- imbi_api-2.11.6/tests/test_resolution_identity.py +0 -49
- {imbi_api-2.11.6 → imbi_api-2.12.2}/.github/workflows/deploy.yaml +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/.github/workflows/docs.yaml +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/.github/workflows/testing.yaml +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/.gitignore +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/.pre-commit-config.yaml +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/.python-version +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/CLAUDE.md +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/LICENSE +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/README.md +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/coderabbit.yaml +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/compose.yaml +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/docs/adr/0001-record-architecture-decisions.md +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/docs/adr/0002-authentication-and-authorization-architecture.md +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/docs/adr/0003-email-sending-architecture.md +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/docs/adr/0004-phase-5-authentication-enhancements.md +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/docs/adr/0005-file-upload-storage-architecture.md +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/docs/adr/0006-project-identity-and-multi-type.md +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/docs/adr/0007-relationship-blueprints.md +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/docs/adr/0008-plugin-system-architecture.md +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/docs/adr/0009-database-driven-oauth-providers.md +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/docs/adr/0010-identity-plugin-architecture.md +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/docs/adr/0011-graph-based-project-scoring.md +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/docs/adr/0012-plugin-manifest-service-template.md +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/docs/adr/0013-deployment-plugin-type.md +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/docs/adr/0014-generic-plugin-entity-abstraction.md +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/docs/adr/0015-cyclonedx-1.7-sbom-standard.md +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/docs/configuration.md +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/docs/index.md +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/docs/restore-backup.md +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/justfile +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/mkdocs.yml +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/mypy.ini +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/__init__.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/app.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/auth/__init__.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/auth/authorization_codes.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/auth/local_auth.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/auth/membership.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/auth/oauth_clients.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/auth/password.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/auth/sessions.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/auth/tokens.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/auth/totp.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/blueprint_attributes.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/blueprint_compliance.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/commit_sync/__init__.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/commit_sync/queue.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/domain/__init__.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/domain/scoring.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/email/__init__.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/email/client.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/email/dependencies.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/email/models.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/email/templates/base.html +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/email/templates/base.txt +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/email/templates/password_reset.html +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/email/templates/password_reset.txt +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/email/templates/welcome.html +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/email/templates/welcome.txt +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/email/templates.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/_credentials.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/_pagination.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/_request_urls.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/admin.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/api_keys.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/client_credentials.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/dashboard.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/document_templates.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/documents.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/environments.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/graph_query.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/link_definitions.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/local_auth.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/mcp_servers.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/mfa.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/oauth_metadata.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/operations_log.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/plugin_edges.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/plugin_entities.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/project_lifecycle.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/project_types.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/releases.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/roles.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/sa_api_keys.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/scoring.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/scoring_policies.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/service_accounts.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/status.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/tags.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/teams.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/endpoints/uploads.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/graph_sql.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/identity/__init__.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/lifespans.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/llm/__init__.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/llm/dependencies.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/mcp_test.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/middleware/__init__.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/middleware/rate_limit.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/openapi.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/patch.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/plugins/__init__.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/plugins/installer.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/plugins/reload.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/plugins/schemas.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/pr_sync/__init__.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/pr_sync/queue.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/prompts/__init__.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/prompts/release_notes_system.md +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/py.typed +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/relationships.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/sbom.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/scoring/__init__.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/scoring/queue.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/settings.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/storage/__init__.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/storage/client.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/storage/dependencies.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/storage/thumbnails.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/src/imbi_api/storage/validation.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/__init__.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/auth/__init__.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/auth/test_api_key_auth.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/auth/test_authentication.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/auth/test_authorization.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/auth/test_encryption.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/auth/test_membership.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/auth/test_oauth_server.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/auth/test_permissions.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/auth/test_sessions.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/auth/test_totp.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/clickhouse/__init__.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/clickhouse/test_client.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/clickhouse/test_init.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/clickhouse/test_privacy.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/email/__init__.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/email/test_client.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/email/test_init.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/email/test_integration.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/email/test_models.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/email/test_templates.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/__init__.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_admin.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_api_keys.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_blueprints.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_client_credentials.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_comments.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_credentials.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_dashboard.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_document_templates.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_documents.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_environments.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_graph_query.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_init.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_json_fields.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_link_definitions.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_local_auth.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_mcp_servers.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_mfa.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_operations_log.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_organizations.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_pagination.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_project_types.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_projects_helpers.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_pull_requests.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_request_urls.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_roles.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_sa_api_keys.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_scoring.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_scoring_policies.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_search.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_service_accounts.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_status.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_tags.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_teams.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_uploads.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/endpoints/test_user_activity.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/fixtures/sbom/npm-realistic.json +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/fixtures/sbom/pypi-realistic.json +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/fixtures/sbom/tiny.json +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/identity/__init__.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/middleware/test_rate_limit.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/storage/__init__.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/storage/test_client.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/storage/test_init.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/storage/test_thumbnails.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/storage/test_validation.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_app.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_backfill_embeddings.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_blueprint_attributes.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_blueprint_compliance.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_blueprints.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_commit_sync_queue.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_graph_sql.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_init.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_installer.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_lifespans.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_openapi.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_patch.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_plugin_schemas.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_pr_sync_queue.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_sbom.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_scoring_queue.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_scoring_triggers.py +0 -0
- {imbi_api-2.11.6 → imbi_api-2.12.2}/tests/test_settings.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: imbi-api
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.12.2
|
|
4
4
|
Summary: Imbi is a DevOps Service Management Platform designed to provide an efficient way to manage a large environment that contains many services and applications.
|
|
5
5
|
Author-email: Alex Campbell <alexc@aweber.com>, Dave Shawley <daves@aweber.com>, "Gavin M. Roy" <gavinr@aweber.com>
|
|
6
6
|
License: BSD-3-Clause
|
|
@@ -20,7 +20,7 @@ Requires-Dist: cyclonedx-python-lib<12,>=11.7
|
|
|
20
20
|
Requires-Dist: fastapi
|
|
21
21
|
Requires-Dist: filetype
|
|
22
22
|
Requires-Dist: httpx<1,>=0.28.1
|
|
23
|
-
Requires-Dist: imbi-common[databases,llm,sentry,server]==2.
|
|
23
|
+
Requires-Dist: imbi-common[databases,llm,sentry,server]==2.12.2
|
|
24
24
|
Requires-Dist: jinja2
|
|
25
25
|
Requires-Dist: jsonpatch>=1.33
|
|
26
26
|
Requires-Dist: mcp>=1.26.0
|
|
@@ -42,13 +42,6 @@ Requires-Dist: opentelemetry-instrumentation-asyncio; extra == 'otel'
|
|
|
42
42
|
Requires-Dist: opentelemetry-instrumentation-fastapi; extra == 'otel'
|
|
43
43
|
Requires-Dist: opentelemetry-instrumentation-httpx; extra == 'otel'
|
|
44
44
|
Requires-Dist: opentelemetry-instrumentation-jinja2; extra == 'otel'
|
|
45
|
-
Provides-Extra: plugins
|
|
46
|
-
Requires-Dist: imbi-plugin-aws>=2.1.0; extra == 'plugins'
|
|
47
|
-
Requires-Dist: imbi-plugin-github>=2.1.0; extra == 'plugins'
|
|
48
|
-
Requires-Dist: imbi-plugin-logzio>=2.1.0; extra == 'plugins'
|
|
49
|
-
Requires-Dist: imbi-plugin-oidc>=2.1.0; extra == 'plugins'
|
|
50
|
-
Requires-Dist: imbi-plugin-pagerduty>=2.1.0; extra == 'plugins'
|
|
51
|
-
Requires-Dist: imbi-plugin-sonarqube>=2.1.0; extra == 'plugins'
|
|
52
45
|
Provides-Extra: sentry
|
|
53
46
|
Requires-Dist: sentry-sdk; extra == 'sentry'
|
|
54
47
|
Description-Content-Type: text/markdown
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# ADR 0016: Delegated Execution for Scheduled Jobs
|
|
2
|
+
|
|
3
|
+
Date: 2026-06-29
|
|
4
|
+
|
|
5
|
+
## Status
|
|
6
|
+
|
|
7
|
+
Accepted
|
|
8
|
+
|
|
9
|
+
Implementation is phased; the full design lives in
|
|
10
|
+
[`imbi/docs/imbi-scheduler-functional-spec.md`](https://github.com/AWeber-Imbi/imbi/blob/main/docs/imbi-scheduler-functional-spec.md).
|
|
11
|
+
This ADR records the `imbi-api`-side decision only — the new delegation
|
|
12
|
+
primitive and the migration of the in-process scheduled loops off the API
|
|
13
|
+
process. The standalone `imbi-scheduler` service itself is a separate repo and
|
|
14
|
+
not in scope for this ADR log.
|
|
15
|
+
|
|
16
|
+
## Context
|
|
17
|
+
|
|
18
|
+
Imbi has grown to need general-purpose scheduled execution, and the platform
|
|
19
|
+
has accumulated scheduling-like behavior in an ad-hoc way:
|
|
20
|
+
|
|
21
|
+
1. **No general scheduler exists.** There is no APScheduler, cron daemon, job
|
|
22
|
+
registry, or recurring-job domain model anywhere in the stack. The graph
|
|
23
|
+
vlabel set (`imbi_common/graph/schemata.toml`) has no `Schedule`/`Job`/
|
|
24
|
+
`Task`/`Trigger` label. `pg_cron` is preloaded and `CREATE EXTENSION`'d but
|
|
25
|
+
unused.
|
|
26
|
+
|
|
27
|
+
2. **The only existing "scheduled" job is hand-rolled.** `imbi_api/scoring/
|
|
28
|
+
queue.py:451-510` (`run_daily_tick`) is a polling loop on `asyncio.sleep`
|
|
29
|
+
that fires once per UTC day at `DAILY_TICK_HOUR_UTC=6`, using a Valkey
|
|
30
|
+
`SETNX` 25h-TTL lock keyed `imbi:score-recompute:daily:<date>` for
|
|
31
|
+
cross-worker single-firing, then enqueues every project for
|
|
32
|
+
`scheduled_recompute`. It is hardcoded to one task and not generalizable.
|
|
33
|
+
Three more in-process consumer loops (commit-sync, PR-sync,
|
|
34
|
+
identity-sweeper) are wired through `lifespans.py` and run inside the API
|
|
35
|
+
process.
|
|
36
|
+
|
|
37
|
+
3. **`imbi-automations` is a one-shot CLI, not a scheduler.** It runs workflow
|
|
38
|
+
TOMLs across projects when invoked manually (`just run <workflow>`); it has
|
|
39
|
+
no recurring-execution capability. The missing piece is a trigger layer.
|
|
40
|
+
|
|
41
|
+
4. **Planned features need scheduling.** Dynamic blueprint-field
|
|
42
|
+
recalculation (e.g. `min(date) where foo = bar` over an applied blueprint
|
|
43
|
+
field) requires periodic re-evaluation. There is no mechanism to schedule
|
|
44
|
+
that today.
|
|
45
|
+
|
|
46
|
+
The platform has decided to introduce a **standalone `imbi-scheduler` service**
|
|
47
|
+
(APScheduler 4.x, owning its own job store and run history) that triggers
|
|
48
|
+
`imbi-api` REST calls on schedules. See the functional spec for that service's
|
|
49
|
+
design; this ADR covers what `imbi-api` must provide for it to work.
|
|
50
|
+
|
|
51
|
+
The hard requirement that drives this ADR: scheduled jobs must be able to
|
|
52
|
+
**execute as the scheduling user** — i.e. with that user's identity and
|
|
53
|
+
permissions, including resource-level `CAN_ACCESS` ACLs — and also as the
|
|
54
|
+
scheduler's own service account for system jobs.
|
|
55
|
+
|
|
56
|
+
The blocker: **`imbi-api` has no impersonation or delegation primitive.**
|
|
57
|
+
`AuthContext` (`auth/permissions.py:43-86`) is always the principal that
|
|
58
|
+
presented the token. Service accounts cannot use resource-level ACLs
|
|
59
|
+
(`permissions.py:907-913` — the `CAN_ACCESS` fallback is `auth.user`-only),
|
|
60
|
+
and admin bypass is `User`-only (`permissions.py:745`). There is no "act on
|
|
61
|
+
behalf of" path, no `act`/`azp` claim, and no consent model. So "run as a user"
|
|
62
|
+
cannot be satisfied by any existing mechanism.
|
|
63
|
+
|
|
64
|
+
## Decision
|
|
65
|
+
|
|
66
|
+
Add a **delegated-execution primitive** to `imbi-api` based on OAuth2 token
|
|
67
|
+
exchange (RFC 8693), plus a **consent model**, so the scheduler can obtain
|
|
68
|
+
short-lived JWTs scoped to a target user. The scheduler holds no user
|
|
69
|
+
secrets.
|
|
70
|
+
|
|
71
|
+
### 1. Token-exchange grant
|
|
72
|
+
|
|
73
|
+
Add a branch to `POST /auth/token` (`endpoints/auth.py:325-499`) for:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
grant_type = urn:ietf:params:oauth:grant-type:token-exchange
|
|
77
|
+
subject_token = <scheduler SA client-credential JWT> (or client_id+secret)
|
|
78
|
+
requested_subject = <target user email>
|
|
79
|
+
scope = <scope covered by a Consent>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`imbi-api` validates that (a) the scheduler SA is authenticated and holds the
|
|
83
|
+
new `auth:delegate` permission, (b) a non-expired, non-revoked `Consent`
|
|
84
|
+
exists for `(user, 'imbi-scheduler', scope ⊇ requested)`, and (c) the target
|
|
85
|
+
user is active. It then mints a short-lived access JWT via
|
|
86
|
+
`imbi_common.auth.core.create_access_token` carrying:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
sub = <target user email>
|
|
90
|
+
auth_method = 'token_exchange'
|
|
91
|
+
act = { client_id: <scheduler SA> } # RFC 8693 `act` claim
|
|
92
|
+
azp = <scheduler SA slug>
|
|
93
|
+
scope = <granted scope>
|
|
94
|
+
jti = new; TokenMetadata node ISSUED_TO the user, linked to the SA
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 2. Consent model
|
|
98
|
+
|
|
99
|
+
Add a **`Consent`** graph node recording that a user authorized the scheduler
|
|
100
|
+
to act on their behalf for a scope. `Consent` is recorded under the **user's
|
|
101
|
+
own** authenticated session (the scheduler passes the user's JWT through when
|
|
102
|
+
creating a schedule), so `imbi-api` attributes consent to the real user — the
|
|
103
|
+
scheduler never records consent on a user's behalf:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
Consent:
|
|
107
|
+
user_email, actor_sa ('imbi-scheduler'), scope,
|
|
108
|
+
created_at, expires_at?, revoked_at?
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Endpoints: `POST /consents` (records under caller identity), `GET /consents`,
|
|
112
|
+
`DELETE /consents/{id}` (revoke). v1 uses one broad scope (`imbi-api:*`) per
|
|
113
|
+
user; finer per-resource-type scopes are a deferred refinement.
|
|
114
|
+
|
|
115
|
+
### 3. `AuthContext.actor`
|
|
116
|
+
|
|
117
|
+
`AuthContext` gains an `actor: ServiceAccount | None` field populated when
|
|
118
|
+
`auth_method == 'token_exchange'`. Permission resolution uses the **user's**
|
|
119
|
+
permissions (including `CAN_ACCESS` resource ACLs — the whole point of
|
|
120
|
+
delegation). `principal_name` remains the user; a new `actor_name` property
|
|
121
|
+
returns the scheduler SA. Audit/`requested_by`-style attribution records
|
|
122
|
+
`"{user} on behalf of {actor}"`. The `act`/`azp` claim is verified on every
|
|
123
|
+
request, not only at exchange.
|
|
124
|
+
|
|
125
|
+
### 4. New permission & service account
|
|
126
|
+
|
|
127
|
+
Seed a new `auth:delegate` permission. Provision an `imbi-scheduler` service
|
|
128
|
+
account (`POST /service-accounts`) with a client credential
|
|
129
|
+
(`POST /service-accounts/{slug}/client-credentials`) whose role grants
|
|
130
|
+
`auth:delegate` plus the global permissions each system job needs (resource
|
|
131
|
+
ACLs are user-only, so system jobs require global perms — document the set
|
|
132
|
+
per migrated task). System jobs authenticate directly with the SA's
|
|
133
|
+
client-credential JWT (refreshed proactively, the
|
|
134
|
+
`imbi-automations/clients/imbi.py:_AuthManager` pattern); user jobs exchange
|
|
135
|
+
for a delegated token at fire time.
|
|
136
|
+
|
|
137
|
+
### 5. Migrate the score-recompute daily tick
|
|
138
|
+
|
|
139
|
+
Externalize the trigger of `run_daily_tick` to the scheduler; **keep the
|
|
140
|
+
worker/queue logic in `imbi-api`** (the `imbi:score-recompute` Valkey-stream
|
|
141
|
+
consumer stays). Concretely: expose (or reuse) an HTTP trigger —
|
|
142
|
+
`POST /api/scoring/recompute-all` guarded by a new
|
|
143
|
+
`scoring:trigger-scheduled` (or `scoring:recompute`) permission — that
|
|
144
|
+
enqueues every project with reason `scheduled_recompute`. Remove
|
|
145
|
+
`run_daily_tick` and its task from `score_worker_hook`
|
|
146
|
+
(`lifespans.py:202-237`); keep `consume_recompute`. The scheduler seeds a
|
|
147
|
+
system schedule `0 6 * * *` → `POST /api/scoring/recompute-all`. The Valkey
|
|
148
|
+
per-date `SETNX` dedupe lock is no longer needed (APScheduler's shared data
|
|
149
|
+
store guarantees single firing across replicas); the existing per-project
|
|
150
|
+
debounce + idempotency in `enqueue_recompute` remains as a backstop.
|
|
151
|
+
|
|
152
|
+
The remaining in-process loops (identity-token sweeper, commit/PR-sync pause
|
|
153
|
+
sweeps) are assessed per-task as follow-ups; only score recompute is in the
|
|
154
|
+
first cut.
|
|
155
|
+
|
|
156
|
+
## Alternatives Considered
|
|
157
|
+
|
|
158
|
+
### A. Per-user API keys stored by the scheduler
|
|
159
|
+
|
|
160
|
+
Users mint `ik_…` API keys and register them with the scheduler, which stores
|
|
161
|
+
them encrypted and replays them at fire time. **Rejected:** the scheduler
|
|
162
|
+
becomes a custodian of long-lived user secrets; keys don't expire unless
|
|
163
|
+
rotated; revocation is manual and scattered; and it conflates "a credential
|
|
164
|
+
the user happened to create" with "an authorization to act." Token exchange
|
|
165
|
+
keeps secrets server-side and makes consent an explicit, revocable record.
|
|
166
|
+
|
|
167
|
+
### B. Run everything as a service account with `requested_by` attribution only
|
|
168
|
+
|
|
169
|
+
Every job runs as the scheduler SA; the originating user is recorded as
|
|
170
|
+
`requested_by`/`actor` for audit only, not as the security principal.
|
|
171
|
+
**Rejected for user jobs:** this does not satisfy "execute as them." The job
|
|
172
|
+
would run with the SA's global permissions and could not pass resource-level
|
|
173
|
+
`CAN_ACCESS` checks, so a user-scheduled job targeting a resource they can
|
|
174
|
+
access but the SA cannot would 403. This remains the model for **system**
|
|
175
|
+
jobs, where it is correct.
|
|
176
|
+
|
|
177
|
+
### C. `pg_cron` driving DB-side jobs
|
|
178
|
+
|
|
179
|
+
The Postgres image already loads `pg_cron`. **Rejected as the primary
|
|
180
|
+
engine:** it can only run SQL, not call arbitrary `imbi-api` endpoints or
|
|
181
|
+
speak the auth/delegation flow, and it couples scheduling to the database
|
|
182
|
+
process. It may still be useful for pure-SQL maintenance later, but it is not
|
|
183
|
+
the general scheduler. The standalone APScheduler service is the engine.
|
|
184
|
+
|
|
185
|
+
### D. Build the scheduler inside `imbi-api` (in-process)
|
|
186
|
+
|
|
187
|
+
Add APScheduler to `imbi-api` and run schedules as a lifespan loop, reusing
|
|
188
|
+
the existing auth context directly. **Rejected:** the platform has
|
|
189
|
+
explicitly chosen a standalone scheduler repo (separable deploy/scaling,
|
|
190
|
+
independent failure domain, and a clean home for run-history that does not
|
|
191
|
+
belong in the graph). The delegation primitive is precisely what lets a
|
|
192
|
+
separate service run as a user safely.
|
|
193
|
+
|
|
194
|
+
## Consequences
|
|
195
|
+
|
|
196
|
+
### Positive
|
|
197
|
+
|
|
198
|
+
- **True run-as-user.** Delegated JWTs run with the user's full permissions,
|
|
199
|
+
including resource ACLs that service accounts cannot use — the capability
|
|
200
|
+
that did not exist before.
|
|
201
|
+
- **No stored user secrets.** The scheduler stores a `consent_id`, not user
|
|
202
|
+
tokens or passwords. The decisive security advantage over per-user keys.
|
|
203
|
+
- **Explicit, revocable authorization.** Consent is a first-class, auditable
|
|
204
|
+
graph node; revoking a schedule or consent stops future exchanges.
|
|
205
|
+
- **Short-lived, revocable delegated tokens** with `jti`-tracked
|
|
206
|
+
`TokenMetadata`, reusing the existing revocation machinery.
|
|
207
|
+
- **General scheduler replaces a hand-rolled loop.** The score daily tick
|
|
208
|
+
becomes one cron entry; future scheduled work (dynamic field recalculation,
|
|
209
|
+
`imbi-automations` triggers) has a home.
|
|
210
|
+
- **Attribution is honest.** `actor_name` + `principal_name` make "who caused
|
|
211
|
+
this and on whose behalf" explicit in logs and audit.
|
|
212
|
+
|
|
213
|
+
### Negative
|
|
214
|
+
|
|
215
|
+
- **New grant type + `AuthContext.actor` + `Consent` model is real surface
|
|
216
|
+
area** in `imbi-api`: auth is the most security-sensitive part of the
|
|
217
|
+
system, and this touches `authenticate_jwt`, token issuance, and the
|
|
218
|
+
permissions module. Requires thorough tests (consent required, scope
|
|
219
|
+
checked, short-lived, revocable, `act`/`azp` verified on every request).
|
|
220
|
+
- **Two systems must agree on consent lifecycle.** The scheduler stores
|
|
221
|
+
`consent_id`; `imbi-api` owns the `Consent` node. A revoked/consent or
|
|
222
|
+
deleted schedule must suppress firings (the scheduler re-checks consent
|
|
223
|
+
before each exchange and treats a failed exchange as a `skipped` run).
|
|
224
|
+
- **System jobs need global permissions.** Because resource ACLs are
|
|
225
|
+
user-only, every system job's SA role must hold the global permission for
|
|
226
|
+
the endpoint it calls. This widens the scheduler SA's blast radius; it is
|
|
227
|
+
mitigated by scoping the role to exactly the trigger endpoints and by
|
|
228
|
+
preferring delegated (user) jobs where the action is user-scoped.
|
|
229
|
+
- **Migration moves a working path.** The score daily tick works today; moving
|
|
230
|
+
it introduces a transitional risk. Mitigated by keeping per-day idempotency
|
|
231
|
+
so a double-fire is harmless even before APScheduler's single-firing
|
|
232
|
+
guarantee applies.
|
|
233
|
+
|
|
234
|
+
### Risks Accepted
|
|
235
|
+
|
|
236
|
+
- **Broad v1 consent scope (`imbi-api:*`).** A consenting user authorizes the
|
|
237
|
+
scheduler to call any `imbi-api` endpoint as them. Acceptable for an
|
|
238
|
+
internal, admin-curated platform for v1; finer scopes are an explicit
|
|
239
|
+
follow-up (functional spec §15).
|
|
240
|
+
- **Delegated token lifetime (5–15 min).** Chosen short; a job making many
|
|
241
|
+
calls reuses the token until expiry then re-exchanges. Not a refresh-token
|
|
242
|
+
family for v1.
|
|
243
|
+
- **Target-path allow-listing deferred.** v1 allows arbitrary `imbi-api`
|
|
244
|
+
method+path as a job target. If abuse becomes a concern, restrict to an
|
|
245
|
+
allow-list per scope (functional spec §12).
|
|
246
|
+
|
|
247
|
+
## References
|
|
248
|
+
|
|
249
|
+
- [`imbi/docs/imbi-scheduler-functional-spec.md`](https://github.com/AWeber-Imbi/imbi/blob/main/docs/imbi-scheduler-functional-spec.md)
|
|
250
|
+
— Full scheduler service design, data model, REST API, and phased plan.
|
|
251
|
+
- ADR 0002: Authentication and Authorization Architecture (the auth system
|
|
252
|
+
this extends — JWTs, service accounts, API keys, permission model).
|
|
253
|
+
- [RFC 8693: OAuth 2.0 Token Exchange](https://datatracker.ietf.org/doc/html/rfc8693)
|
|
254
|
+
— `act`/`azp` claims and the token-exchange grant type.
|
|
255
|
+
- `imbi_api/auth/permissions.py:43-86,306-417,907-913` — `AuthContext`,
|
|
256
|
+
`authenticate_jwt`, and the user-only `CAN_ACCESS` fallback that motivates
|
|
257
|
+
delegation.
|
|
258
|
+
- `imbi_api/scoring/queue.py:451-510` and `imbi_api/lifespans.py:202-237` —
|
|
259
|
+
the `run_daily_tick` loop being externalized.
|
|
260
|
+
- `imbi-automations/src/imbi_automations/clients/imbi.py:87-180` — the
|
|
261
|
+
`_AuthManager` reference pattern for SA client-credential refresh.
|
|
@@ -285,6 +285,37 @@ Replaces hand-coded per-plugin entity routers (e.g., `aws_accounts.py`) with a g
|
|
|
285
285
|
|
|
286
286
|
---
|
|
287
287
|
|
|
288
|
+
### Operations & Integrations
|
|
289
|
+
|
|
290
|
+
#### [ADR 0015: CycloneDX 1.7 as the SBoM Standard](adr/0015-cyclonedx-1.7-sbom-standard.md)
|
|
291
|
+
|
|
292
|
+
*Date: 2026-05-27 | Status: Accepted*
|
|
293
|
+
|
|
294
|
+
Standardizes the SBoM spec version accepted on `PUT .../releases/{id}/sbom` and the recommended producer for build CI.
|
|
295
|
+
|
|
296
|
+
**Key Decisions:**
|
|
297
|
+
|
|
298
|
+
- Accept CycloneDX 1.7 on the wire (1.5/1.6 silently drop fields Imbi reads)
|
|
299
|
+
- Single recommended producer that covers the polyglot service population and emits the per-component metadata needed for `ReleaseComponentEdge` `scope`/`groups`
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
#### [ADR 0016: Delegated Execution for Scheduled Jobs](adr/0016-delegated-execution-for-scheduled-jobs.md)
|
|
304
|
+
|
|
305
|
+
*Date: 2026-06-29 | Status: Accepted*
|
|
306
|
+
|
|
307
|
+
Adds an OAuth2 token-exchange (RFC 8693) delegation primitive plus a `Consent` model so the standalone `imbi-scheduler` service can run scheduled jobs as a user, and externalizes the score-recompute daily tick off the API process.
|
|
308
|
+
|
|
309
|
+
**Key Decisions:**
|
|
310
|
+
|
|
311
|
+
- New `urn:ietf:params:oauth:grant-type:token-exchange` grant minting short-lived JWTs with `sub=<user>` and an `act`/`azp` claim naming the scheduler SA
|
|
312
|
+
- `Consent` graph node recorded under the user's own session; scheduler stores no user secrets
|
|
313
|
+
- `AuthContext.actor` field + `actor_name`; permission resolution uses the user's permissions (including user-only `CAN_ACCESS` ACLs)
|
|
314
|
+
- New `auth:delegate` permission and `imbi-scheduler` service account; system jobs use the SA directly
|
|
315
|
+
- Score-recompute daily tick (`run_daily_tick`) moves to a scheduler-owned cron; the Valkey-stream consumer stays in `imbi-api`
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
288
319
|
## ADR Format
|
|
289
320
|
|
|
290
321
|
Our ADRs follow this structure:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "imbi-api"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.12.2"
|
|
4
4
|
description = "Imbi is a DevOps Service Management Platform designed to provide an efficient way to manage a large environment that contains many services and applications."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.14"
|
|
@@ -28,7 +28,7 @@ dependencies = [
|
|
|
28
28
|
"fastapi",
|
|
29
29
|
"filetype",
|
|
30
30
|
"httpx>=0.28.1,<1",
|
|
31
|
-
"imbi-common[databases,llm,sentry,server]==2.
|
|
31
|
+
"imbi-common[databases,llm,sentry,server]==2.12.2",
|
|
32
32
|
"jinja2",
|
|
33
33
|
"jsonpatch>=1.33",
|
|
34
34
|
"mcp>=1.26.0",
|
|
@@ -57,14 +57,6 @@ otel = [
|
|
|
57
57
|
"opentelemetry-instrumentation-httpx",
|
|
58
58
|
"opentelemetry-instrumentation-jinja2",
|
|
59
59
|
]
|
|
60
|
-
plugins = [
|
|
61
|
-
"imbi-plugin-aws>=2.1.0",
|
|
62
|
-
"imbi-plugin-github>=2.1.0",
|
|
63
|
-
"imbi-plugin-logzio>=2.1.0",
|
|
64
|
-
"imbi-plugin-oidc>=2.1.0",
|
|
65
|
-
"imbi-plugin-pagerduty>=2.1.0",
|
|
66
|
-
"imbi-plugin-sonarqube>=2.1.0"
|
|
67
|
-
]
|
|
68
60
|
sentry = ["sentry-sdk"]
|
|
69
61
|
|
|
70
62
|
[dependency-groups]
|
|
@@ -231,7 +223,7 @@ max-complexity = 15
|
|
|
231
223
|
|
|
232
224
|
[tool.uv]
|
|
233
225
|
exclude-newer = "7 days"
|
|
234
|
-
exclude-newer-package = { "clickhouse-connect" = false, "imbi-common" = false
|
|
226
|
+
exclude-newer-package = { "clickhouse-connect" = false, "imbi-common" = false }
|
|
235
227
|
|
|
236
228
|
[[tool.uv.index]]
|
|
237
229
|
url = "https://pypi.org/simple"
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Login-provider repository.
|
|
2
|
+
|
|
3
|
+
Plugin Architecture v3: a login provider is an ``Integration`` whose
|
|
4
|
+
plugin declares an ``identity`` capability with the ``login_capable``
|
|
5
|
+
hint, and whose node carries a direct ``used_as_login=true`` property
|
|
6
|
+
(set by the admin UI when promoting the Integration to a login
|
|
7
|
+
provider). There is exactly one source now -- the legacy
|
|
8
|
+
``ServiceApplication`` / hardcoded ``google``/``github``/``oidc`` rows
|
|
9
|
+
and the separate ``:Plugin`` node lookup are both gone.
|
|
10
|
+
|
|
11
|
+
A 30s in-memory TTL cache keeps the OAuth hot path off the graph.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
import time
|
|
18
|
+
import typing
|
|
19
|
+
|
|
20
|
+
import pydantic
|
|
21
|
+
from imbi_common import graph
|
|
22
|
+
from imbi_common.plugins import (
|
|
23
|
+
PluginNotFoundError,
|
|
24
|
+
decrypt_integration_credentials,
|
|
25
|
+
get_plugin,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
from imbi_api import settings
|
|
29
|
+
from imbi_api.plugins.assignments import (
|
|
30
|
+
capability_enabled,
|
|
31
|
+
hydrate_integration,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
LOGGER = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
_CACHE_TTL_SECONDS = 30.0
|
|
37
|
+
|
|
38
|
+
_provider_cache: dict[str, tuple[LoginApp, float]] = {}
|
|
39
|
+
_list_cache: dict[bool, tuple[list[LoginApp], float]] = {}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class LoginApp(pydantic.BaseModel):
|
|
43
|
+
"""Flat view of a login-eligible Integration.
|
|
44
|
+
|
|
45
|
+
Combines the Integration's decrypted OAuth credentials with its
|
|
46
|
+
``options`` (issuer URL, OAuth endpoints, etc.) so callers don't
|
|
47
|
+
need to re-query the graph or decrypt anything themselves.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
model_config = pydantic.ConfigDict(extra='ignore')
|
|
51
|
+
|
|
52
|
+
slug: str
|
|
53
|
+
name: str
|
|
54
|
+
integration_id: str
|
|
55
|
+
#: The plugin's declared ``auth_type`` (``oauth2``, ``oidc``,
|
|
56
|
+
#: ``aws-iam-ic``, ...), or ``integration.options['oauth_app_type']``
|
|
57
|
+
#: when the plugin overrides it.
|
|
58
|
+
oauth_app_type: str
|
|
59
|
+
client_id: str | None = None
|
|
60
|
+
client_secret: str | None = None
|
|
61
|
+
issuer_url: str | None = None
|
|
62
|
+
allowed_domains: list[str] = pydantic.Field(default_factory=list)
|
|
63
|
+
scopes: list[str] = pydantic.Field(default_factory=list)
|
|
64
|
+
status: str = 'active'
|
|
65
|
+
authorization_endpoint: str | None = None
|
|
66
|
+
token_endpoint: str | None = None
|
|
67
|
+
revoke_endpoint: str | None = None
|
|
68
|
+
callback_url: str = ''
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def invalidate_cache(slug: str | None = None) -> None:
|
|
72
|
+
"""Drop cached entries.
|
|
73
|
+
|
|
74
|
+
``slug=None`` clears everything; otherwise only the slug + lists.
|
|
75
|
+
"""
|
|
76
|
+
if slug is None:
|
|
77
|
+
_provider_cache.clear()
|
|
78
|
+
else:
|
|
79
|
+
_provider_cache.pop(slug, None)
|
|
80
|
+
_list_cache.clear()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
_LOGIN_INTEGRATIONS_QUERY: typing.LiteralString = """
|
|
84
|
+
MATCH (i:Integration)
|
|
85
|
+
WHERE i.used_as_login = true
|
|
86
|
+
RETURN i
|
|
87
|
+
ORDER BY i.slug
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
_LOGIN_INTEGRATION_QUERY: typing.LiteralString = """
|
|
91
|
+
MATCH (i:Integration {{slug: {slug}}})
|
|
92
|
+
WHERE i.used_as_login = true
|
|
93
|
+
RETURN i
|
|
94
|
+
LIMIT 1
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _integration_to_login_app(
|
|
99
|
+
integration: dict[str, typing.Any],
|
|
100
|
+
) -> LoginApp | None:
|
|
101
|
+
"""Materialize a ``LoginApp`` from a hydrated Integration, or
|
|
102
|
+
``None`` when its plugin doesn't declare a login-capable identity
|
|
103
|
+
capability or the capability is disabled."""
|
|
104
|
+
plugin_slug = str(integration.get('plugin') or '')
|
|
105
|
+
try:
|
|
106
|
+
entry = get_plugin(plugin_slug)
|
|
107
|
+
except PluginNotFoundError:
|
|
108
|
+
return None
|
|
109
|
+
capability = entry.manifest.get_capability('identity')
|
|
110
|
+
if capability is None or not capability.hints.get('login_capable'):
|
|
111
|
+
return None
|
|
112
|
+
if not capability_enabled(integration, 'identity'):
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
# An Integration with no stable ``id`` cannot back a login flow
|
|
116
|
+
# (``load_integration`` matches on ``id``); skip it rather than let a
|
|
117
|
+
# single malformed node 500 the whole login page.
|
|
118
|
+
integration_id = integration.get('id')
|
|
119
|
+
if not integration_id:
|
|
120
|
+
LOGGER.warning(
|
|
121
|
+
'Integration %r is marked used_as_login but has no id; skipping',
|
|
122
|
+
integration.get('slug'),
|
|
123
|
+
)
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
options = typing.cast(
|
|
127
|
+
'dict[str, typing.Any]', integration.get('options') or {}
|
|
128
|
+
)
|
|
129
|
+
credentials = decrypt_integration_credentials(
|
|
130
|
+
typing.cast(
|
|
131
|
+
'dict[str, str]', integration.get('encrypted_credentials') or {}
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
slug = str(integration['slug'])
|
|
135
|
+
return LoginApp(
|
|
136
|
+
slug=slug,
|
|
137
|
+
name=str(integration.get('name') or slug),
|
|
138
|
+
integration_id=str(integration_id),
|
|
139
|
+
oauth_app_type=str(
|
|
140
|
+
options.get('oauth_app_type') or entry.manifest.auth_type
|
|
141
|
+
),
|
|
142
|
+
client_id=credentials.get('client_id'),
|
|
143
|
+
client_secret=credentials.get('client_secret'),
|
|
144
|
+
issuer_url=options.get('issuer_url'),
|
|
145
|
+
allowed_domains=list(options.get('allowed_domains') or []),
|
|
146
|
+
scopes=list(options.get('scopes') or []),
|
|
147
|
+
status=str(integration.get('status') or 'active'),
|
|
148
|
+
authorization_endpoint=options.get('authorization_endpoint'),
|
|
149
|
+
token_endpoint=options.get('token_endpoint'),
|
|
150
|
+
revoke_endpoint=options.get('revoke_endpoint'),
|
|
151
|
+
callback_url=settings.oauth_callback_url(slug),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
async def list_login_apps(
|
|
156
|
+
db: graph.Graph,
|
|
157
|
+
*,
|
|
158
|
+
enabled_only: bool = False,
|
|
159
|
+
) -> list[LoginApp]:
|
|
160
|
+
"""Return every login-eligible Integration.
|
|
161
|
+
|
|
162
|
+
Cached per ``enabled_only`` for ``_CACHE_TTL_SECONDS``.
|
|
163
|
+
"""
|
|
164
|
+
cached = _list_cache.get(enabled_only)
|
|
165
|
+
now = time.time()
|
|
166
|
+
if cached is not None and (now - cached[1]) < _CACHE_TTL_SECONDS:
|
|
167
|
+
return list(cached[0])
|
|
168
|
+
|
|
169
|
+
records = await db.execute(_LOGIN_INTEGRATIONS_QUERY, {}, ['i'])
|
|
170
|
+
apps: list[LoginApp] = []
|
|
171
|
+
for record in records:
|
|
172
|
+
props: typing.Any = graph.parse_agtype(record['i'])
|
|
173
|
+
if not isinstance(props, dict):
|
|
174
|
+
continue
|
|
175
|
+
typed_props = typing.cast('dict[str, typing.Any]', props)
|
|
176
|
+
login_app = _integration_to_login_app(hydrate_integration(typed_props))
|
|
177
|
+
if login_app is None:
|
|
178
|
+
continue
|
|
179
|
+
if enabled_only and login_app.status != 'active':
|
|
180
|
+
continue
|
|
181
|
+
apps.append(login_app)
|
|
182
|
+
|
|
183
|
+
_list_cache[enabled_only] = (list(apps), now)
|
|
184
|
+
return apps
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
async def get_login_app(db: graph.Graph, slug: str) -> LoginApp | None:
|
|
188
|
+
"""Look up a single login app by Integration slug."""
|
|
189
|
+
cached = _provider_cache.get(slug)
|
|
190
|
+
now = time.time()
|
|
191
|
+
if cached is not None and (now - cached[1]) < _CACHE_TTL_SECONDS:
|
|
192
|
+
return cached[0]
|
|
193
|
+
|
|
194
|
+
records = await db.execute(_LOGIN_INTEGRATION_QUERY, {'slug': slug}, ['i'])
|
|
195
|
+
if not records:
|
|
196
|
+
return None
|
|
197
|
+
props: typing.Any = graph.parse_agtype(records[0]['i'])
|
|
198
|
+
if not isinstance(props, dict):
|
|
199
|
+
return None
|
|
200
|
+
typed_props = typing.cast('dict[str, typing.Any]', props)
|
|
201
|
+
login_app = _integration_to_login_app(hydrate_integration(typed_props))
|
|
202
|
+
if login_app is None:
|
|
203
|
+
return None
|
|
204
|
+
_provider_cache[slug] = (login_app, now)
|
|
205
|
+
return login_app
|
|
@@ -53,7 +53,7 @@ class OAuthStateData(pydantic.BaseModel):
|
|
|
53
53
|
* ``intent`` discriminates ``'login'`` from ``'identity'``. Login
|
|
54
54
|
flows still create the local user; identity flows persist an
|
|
55
55
|
:class:`imbi_common.models.IdentityConnection` for the actor.
|
|
56
|
-
* ``
|
|
56
|
+
* ``integration_id`` names the target Integration (or ``None`` for
|
|
57
57
|
the legacy hardcoded login providers).
|
|
58
58
|
* ``code_verifier`` carries the PKCE verifier through the redirect
|
|
59
59
|
so we don't need server-side state for in-flight flows.
|
|
@@ -72,7 +72,7 @@ class OAuthStateData(pydantic.BaseModel):
|
|
|
72
72
|
redirect_uri: str # Where to redirect after auth
|
|
73
73
|
timestamp: int # Unix timestamp for expiry
|
|
74
74
|
intent: typing.Literal['login', 'identity'] = 'login'
|
|
75
|
-
|
|
75
|
+
integration_id: str | None = None
|
|
76
76
|
code_verifier: str | None = None
|
|
77
77
|
return_to: str | None = None
|
|
78
78
|
actor_user_id: str | None = None
|
|
@@ -13,7 +13,6 @@ from urllib import parse as urlparse
|
|
|
13
13
|
import httpx
|
|
14
14
|
import jwt
|
|
15
15
|
from imbi_common import graph
|
|
16
|
-
from imbi_common.auth import encryption
|
|
17
16
|
from valkey import asyncio as valkey_module
|
|
18
17
|
|
|
19
18
|
from imbi_api import settings
|
|
@@ -279,7 +278,7 @@ async def exchange_oauth_code(
|
|
|
279
278
|
"""Exchange OAuth authorization code for tokens.
|
|
280
279
|
|
|
281
280
|
Args:
|
|
282
|
-
slug:
|
|
281
|
+
slug: Login-Integration slug
|
|
283
282
|
code: Authorization code from provider
|
|
284
283
|
redirect_uri: Redirect URI used in authorization request
|
|
285
284
|
db: Graph database used to look up provider configuration
|
|
@@ -327,7 +326,7 @@ async def fetch_oauth_profile(
|
|
|
327
326
|
"""Fetch user profile from OAuth provider.
|
|
328
327
|
|
|
329
328
|
Args:
|
|
330
|
-
slug:
|
|
329
|
+
slug: Login-Integration slug
|
|
331
330
|
access_token: Access token from provider
|
|
332
331
|
db: Graph database used to look up provider configuration
|
|
333
332
|
|
|
@@ -446,17 +445,6 @@ async def _load_active_login_app(
|
|
|
446
445
|
return app
|
|
447
446
|
|
|
448
447
|
|
|
449
|
-
def _decrypt_secret(app: login_providers.LoginApp) -> str:
|
|
450
|
-
"""Decrypt the row's ``client_secret_encrypted`` value."""
|
|
451
|
-
if not app.client_secret_encrypted:
|
|
452
|
-
return ''
|
|
453
|
-
encryptor = encryption.TokenEncryption.get_instance()
|
|
454
|
-
secret = encryptor.decrypt(app.client_secret_encrypted)
|
|
455
|
-
if secret is None:
|
|
456
|
-
raise ValueError('Failed to decrypt OAuth client secret')
|
|
457
|
-
return secret
|
|
458
|
-
|
|
459
|
-
|
|
460
448
|
async def _get_provider_config(
|
|
461
449
|
slug: str, db: graph.Graph
|
|
462
450
|
) -> tuple[str, str, str]:
|
|
@@ -467,7 +455,11 @@ async def _get_provider_config(
|
|
|
467
455
|
"""
|
|
468
456
|
app = await _load_active_login_app(slug, db)
|
|
469
457
|
client_id = app.client_id or ''
|
|
470
|
-
client_secret
|
|
458
|
+
# LoginApp.client_secret is already plaintext -- Integration
|
|
459
|
+
# credentials are decrypted once when the login app is materialized
|
|
460
|
+
# (imbi_api.auth.login_providers), unlike the old ServiceApplication
|
|
461
|
+
# row which stored an encrypted value this module used to decrypt.
|
|
462
|
+
client_secret = app.client_secret or ''
|
|
471
463
|
if app.oauth_app_type == 'google':
|
|
472
464
|
return (
|
|
473
465
|
'https://oauth2.googleapis.com/token',
|
|
@@ -480,7 +472,7 @@ async def _get_provider_config(
|
|
|
480
472
|
client_id,
|
|
481
473
|
client_secret,
|
|
482
474
|
)
|
|
483
|
-
# oidc: prefer
|
|
475
|
+
# oidc: prefer the Integration's configured token_endpoint, else discover
|
|
484
476
|
if app.token_endpoint:
|
|
485
477
|
return (app.token_endpoint, client_id, client_secret)
|
|
486
478
|
if not app.issuer_url:
|