fred-core 3.4.4__tar.gz → 3.4.7__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.
Files changed (184) hide show
  1. {fred_core-3.4.4 → fred_core-3.4.7}/PKG-INFO +1 -1
  2. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/__init__.py +9 -0
  3. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/common/__init__.py +2 -0
  4. fred_core-3.4.7/fred_core/common/gcs_client.py +52 -0
  5. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/common/utils.py +2 -1
  6. fred_core-3.4.7/fred_core/conversion/__init__.py +25 -0
  7. fred_core-3.4.7/fred_core/conversion/pptx_pdf.py +147 -0
  8. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/filesystem/gcs_filesystem.py +6 -6
  9. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/security/models.py +11 -2
  10. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/security/oidc.py +53 -0
  11. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/security/rebac/rebac_engine.py +105 -4
  12. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/store/__init__.py +2 -0
  13. fred_core-3.4.7/fred_core/store/gcs_content_store.py +197 -0
  14. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/common/test_fastapi_handlers.py +35 -1
  15. fred_core-3.4.7/fred_core/tests/common/test_gcs_client.py +72 -0
  16. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/common/test_lru_cache.py +4 -2
  17. fred_core-3.4.7/fred_core/tests/conversion/test_pptx_pdf.py +137 -0
  18. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/filesystem/test_gcs_filesystem.py +3 -3
  19. fred_core-3.4.7/fred_core/tests/security/test_oidc_token_lifetime.py +139 -0
  20. fred_core-3.4.7/fred_core/tests/security/test_rebac_engine_team_helpers.py +544 -0
  21. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/security/test_service_agent.py +3 -2
  22. fred_core-3.4.7/fred_core/tests/store/test_gcs_content_store.py +187 -0
  23. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core.egg-info/PKG-INFO +1 -1
  24. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core.egg-info/SOURCES.txt +8 -0
  25. {fred_core-3.4.4 → fred_core-3.4.7}/pyproject.toml +2 -2
  26. fred_core-3.4.4/fred_core/tests/security/test_rebac_engine_team_helpers.py +0 -265
  27. {fred_core-3.4.4 → fred_core-3.4.7}/README.md +0 -0
  28. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/cli/__init__.py +0 -0
  29. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/cli/auth.py +0 -0
  30. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/cli/ui.py +0 -0
  31. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/common/config_files.py +0 -0
  32. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/common/config_loader.py +0 -0
  33. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/common/env.py +0 -0
  34. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/common/fastapi_handlers.py +0 -0
  35. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/common/lru_cache.py +0 -0
  36. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/common/resilient_sink.py +0 -0
  37. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/common/structures.py +0 -0
  38. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/common/team_id.py +0 -0
  39. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/documents/__init__.py +0 -0
  40. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/documents/document_models.py +0 -0
  41. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/documents/document_store.py +0 -0
  42. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/documents/document_structures.py +0 -0
  43. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/documents/postgres_document_store.py +0 -0
  44. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/documents/tag_models.py +0 -0
  45. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/filesystem/local_filesystem.py +0 -0
  46. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/filesystem/minio_filesystem.py +0 -0
  47. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/filesystem/structures.py +0 -0
  48. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/history/__init__.py +0 -0
  49. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/history/base_history_store.py +0 -0
  50. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/history/history_models.py +0 -0
  51. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/history/history_schema.py +0 -0
  52. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/history/postgres_history_store.py +0 -0
  53. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/kpi/__init__.py +0 -0
  54. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/kpi/base_kpi_store.py +0 -0
  55. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/kpi/base_kpi_writer.py +0 -0
  56. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/kpi/http_middleware.py +0 -0
  57. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/kpi/kpi_factory.py +0 -0
  58. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/kpi/kpi_phase_metric.py +0 -0
  59. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/kpi/kpi_process.py +0 -0
  60. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/kpi/kpi_reader_structures.py +0 -0
  61. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/kpi/kpi_writer.py +0 -0
  62. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/kpi/kpi_writer_structures.py +0 -0
  63. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/kpi/log_kpi_store.py +0 -0
  64. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/kpi/noop_kpi_writer.py +0 -0
  65. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/kpi/opensearch_kpi_store.py +0 -0
  66. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/kpi/prometheus_kpi_store.py +0 -0
  67. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/logs/__init__.py +0 -0
  68. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/logs/audit_log.py +0 -0
  69. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/logs/base_log_store.py +0 -0
  70. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/logs/log_setup.py +0 -0
  71. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/logs/log_store_factory.py +0 -0
  72. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/logs/log_structures.py +0 -0
  73. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/logs/memory_log_store.py +0 -0
  74. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/logs/null_log_store.py +0 -0
  75. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/logs/opensearch_log_store.py +0 -0
  76. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/model/factory.py +0 -0
  77. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/model/http_clients.py +0 -0
  78. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/model/models.py +0 -0
  79. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/models/__init__.py +0 -0
  80. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/models/base.py +0 -0
  81. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/portable/__init__.py +0 -0
  82. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/portable/observability.py +0 -0
  83. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/py.typed +0 -0
  84. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/scheduler/__init__.py +0 -0
  85. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/scheduler/backend.py +0 -0
  86. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/scheduler/scheduler_structures.py +0 -0
  87. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/scheduler/temporal_client_provider.py +0 -0
  88. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/security/__init__.py +0 -0
  89. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/security/authorization.py +0 -0
  90. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/security/backend_to_backend_auth.py +0 -0
  91. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/security/keycloak/__init__.py +0 -0
  92. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/security/keycloak/keycloack_admin_client.py +0 -0
  93. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/security/outbound.py +0 -0
  94. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/security/rebac/noop_engine.py +0 -0
  95. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/security/rebac/openfga_engine.py +0 -0
  96. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/security/rebac/openfga_schema.py +0 -0
  97. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/security/rebac/rebac_factory.py +0 -0
  98. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/security/rebac/schema.fga +0 -0
  99. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/security/rebac/schema.fga.json +0 -0
  100. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/security/structure.py +0 -0
  101. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/security/whitelist_access_control/access_control.py +0 -0
  102. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/session/__init__.py +0 -0
  103. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/session/session_schema.py +0 -0
  104. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/session/stores/__init__.py +0 -0
  105. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/session/stores/base_session_store.py +0 -0
  106. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/session/stores/postgres_session_store.py +0 -0
  107. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/session/stores/session_models.py +0 -0
  108. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/sql/__init__.py +0 -0
  109. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/sql/alembic_env.py +0 -0
  110. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/sql/async_session.py +0 -0
  111. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/sql/base_sql.py +0 -0
  112. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/sql/mixin.py +0 -0
  113. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/store/base_content_store.py +0 -0
  114. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/store/local_content_store.py +0 -0
  115. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/store/minio_content_store.py +0 -0
  116. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/store/opensearch_mapping_validator.py +0 -0
  117. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/store/vector_search.py +0 -0
  118. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tasks/__init__.py +0 -0
  119. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tasks/authz.py +0 -0
  120. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tasks/bus.py +0 -0
  121. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tasks/models.py +0 -0
  122. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tasks/orm_models.py +0 -0
  123. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tasks/service.py +0 -0
  124. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tasks/sse.py +0 -0
  125. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tasks/store.py +0 -0
  126. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tasks/workflow_control.py +0 -0
  127. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/teams/__init__.py +0 -0
  128. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/teams/metadata_store.py +0 -0
  129. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/teams/team_metatada_models.py +0 -0
  130. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/__init__.py +0 -0
  131. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/cli/test_auth.py +0 -0
  132. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/cli/test_ui.py +0 -0
  133. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/common/test_config_loader.py +0 -0
  134. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/common/test_env.py +0 -0
  135. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/common/test_log_setup.py +0 -0
  136. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/common/test_resilient_sink.py +0 -0
  137. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/common/test_structures.py +0 -0
  138. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/filesystem/test_local_filesystem.py +0 -0
  139. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/integration/__init__.py +0 -0
  140. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/integration/test_rebac.py +0 -0
  141. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/kpi/test_http_middleware.py +0 -0
  142. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/kpi/test_noop_kpi_writer.py +0 -0
  143. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/kpi/test_opensearch_kpi_store_anonymise.py +0 -0
  144. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/kpi/test_opensearch_kpi_store_ensure_ready.py +0 -0
  145. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/logs/test_audit_log.py +0 -0
  146. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/logs/test_log_store_factory.py +0 -0
  147. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/logs/test_memory_log_store.py +0 -0
  148. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/logs/test_opensearch_log_store_ensure_ready.py +0 -0
  149. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/model/test_anthropic_factory.py +0 -0
  150. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/model/test_embedding_factory.py +0 -0
  151. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/model/test_http_clients.py +0 -0
  152. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/model/test_vertex_model_garden_auth.py +0 -0
  153. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/portable/test_observability.py +0 -0
  154. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/scheduler/test_backend.py +0 -0
  155. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/scheduler/test_temporal_client_provider.py +0 -0
  156. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/security/__init__.py +0 -0
  157. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/security/test_capability_scoping_1980.py +0 -0
  158. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/security/test_oidc_strict.py +0 -0
  159. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/security/test_rebac_config_defaults.py +0 -0
  160. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/security/test_rebac_schema_authz05.py +0 -0
  161. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/security/test_security_profile.py +0 -0
  162. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/security/test_whitelist_access_control.py +0 -0
  163. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/session/test_postgres_json_session_store_sqlite.py +0 -0
  164. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/store/test_local_content_store.py +0 -0
  165. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/store/test_vector_search.py +0 -0
  166. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/tasks/__init__.py +0 -0
  167. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/tasks/test_authz.py +0 -0
  168. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/tasks/test_bus.py +0 -0
  169. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/tasks/test_models.py +0 -0
  170. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/tasks/test_reconcile.py +0 -0
  171. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/tasks/test_scheduling.py +0 -0
  172. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/tasks/test_sse.py +0 -0
  173. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/test_kpi_writer_fail_open.py +0 -0
  174. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/test_log_kpi_store.py +0 -0
  175. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/tests/test_prometheus_kpi_store.py +0 -0
  176. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/users/__init__.py +0 -0
  177. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/users/store/__init__.py +0 -0
  178. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/users/store/base_user_store.py +0 -0
  179. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/users/store/postgres_user_store.py +0 -0
  180. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core/users/user_models.py +0 -0
  181. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core.egg-info/dependency_links.txt +0 -0
  182. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core.egg-info/requires.txt +0 -0
  183. {fred_core-3.4.4 → fred_core-3.4.7}/fred_core.egg-info/top_level.txt +0 -0
  184. {fred_core-3.4.4 → fred_core-3.4.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fred-core
3
- Version: 3.4.4
3
+ Version: 3.4.7
4
4
  Summary: Core shared utilities for Fred backends (config, storage, security, and runtime helpers).
5
5
  Author-email: Thales <noreply@thalesgroup.com>
6
6
  License: Apache-2.0
@@ -11,6 +11,11 @@
11
11
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
+ from fred_core.conversion import (
15
+ DEFAULT_PPTX_PDF_TIMEOUT_SECONDS,
16
+ convert_pptx_bytes_to_pdf,
17
+ convert_pptx_file_to_pdf,
18
+ )
14
19
  from fred_core.documents import (
15
20
  AccessInfo,
16
21
  BaseDocumentMetadataStore,
@@ -208,6 +213,10 @@ __all__ = [
208
213
  "GcuVersionsType",
209
214
  "get_user_store",
210
215
  "get_config",
216
+ # conversion
217
+ "DEFAULT_PPTX_PDF_TIMEOUT_SECONDS",
218
+ "convert_pptx_bytes_to_pdf",
219
+ "convert_pptx_file_to_pdf",
211
220
  "TeamMetadata",
212
221
  "TeamMetadataPatch",
213
222
  "TeamMetadataStore",
@@ -20,6 +20,7 @@ from .config_loader import (
20
20
  )
21
21
  from .env import coerce_bool, read_env_bool
22
22
  from .fastapi_handlers import register_exception_handlers
23
+ from .gcs_client import build_gcs_client
23
24
  from .lru_cache import ThreadSafeLRUCache
24
25
  from .resilient_sink import ResilientSinkStore
25
26
  from .structures import (
@@ -64,6 +65,7 @@ __all__ = [
64
65
  "ResilientSinkStore",
65
66
  "TemporalSchedulerConfig",
66
67
  "ThreadSafeLRUCache",
68
+ "build_gcs_client",
67
69
  "coerce_bool",
68
70
  "load_configuration_with_config_files",
69
71
  "parse_yaml_mapping_file",
@@ -0,0 +1,52 @@
1
+ # Copyright Thales 2026
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import Optional
18
+
19
+ import google.auth
20
+ from google.api_core.client_options import ClientOptions
21
+ from google.cloud import storage
22
+
23
+
24
+ def build_gcs_client(project_id: Optional[str] = None) -> storage.Client:
25
+ """Build a GCS client whose universe domain matches the loaded ADC credentials.
26
+
27
+ Why this exists:
28
+ - Bare ``storage.Client(project=project_id)`` implicitly assumes the
29
+ ``googleapis.com`` universe domain. Under Trusted Partner Cloud /
30
+ sovereign deployments (e.g. S3NS, the Thales/Google joint venture cloud
31
+ in France), Workload Identity Federation ADC credentials declare a
32
+ different universe domain (e.g. ``s3nsapis.fr``), and the client raises
33
+ ``google.api_core.universe.UniverseMismatchError`` the moment it tries
34
+ to authenticate: "The configured universe domain (googleapis.com) does
35
+ not match the universe domain found in the credentials (s3nsapis.fr)."
36
+ - Explicitly loading ADC and mirroring ``credentials.universe_domain``
37
+ into ``client_options`` makes every GCS-backed store portable across
38
+ public GCP and sovereign GCP variants with zero extra configuration —
39
+ the client always trusts whatever universe the credentials actually
40
+ belong to instead of assuming the public default.
41
+
42
+ Used by every native-GCS backend (fred-core's content store and virtual
43
+ filesystem, knowledge-flow's content store and file store) so the fix
44
+ lives in one place.
45
+ """
46
+
47
+ credentials, adc_project = google.auth.default()
48
+ return storage.Client(
49
+ project=project_id or adc_project,
50
+ credentials=credentials,
51
+ client_options=ClientOptions(universe_domain=credentials.universe_domain),
52
+ )
@@ -14,11 +14,12 @@
14
14
 
15
15
  import logging
16
16
  import uuid
17
+ from typing import NoReturn
17
18
 
18
19
  from fastapi import HTTPException
19
20
 
20
21
 
21
- def raise_internal_error(logger: logging.Logger, msg: str, exc: Exception):
22
+ def raise_internal_error(logger: logging.Logger, msg: str, exc: Exception) -> NoReturn:
22
23
  """
23
24
  Raise a FastAPI HTTPException (500) while logging the full exception with a unique error ID.
24
25
 
@@ -0,0 +1,25 @@
1
+ # Copyright Thales 2026
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from fred_core.conversion.pptx_pdf import (
16
+ DEFAULT_PPTX_PDF_TIMEOUT_SECONDS,
17
+ convert_pptx_bytes_to_pdf,
18
+ convert_pptx_file_to_pdf,
19
+ )
20
+
21
+ __all__ = [
22
+ "DEFAULT_PPTX_PDF_TIMEOUT_SECONDS",
23
+ "convert_pptx_bytes_to_pdf",
24
+ "convert_pptx_file_to_pdf",
25
+ ]
@@ -0,0 +1,147 @@
1
+ # Copyright Thales 2026
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Shared, best-effort PPTX→PDF conversion using headless LibreOffice.
16
+
17
+ Why this lives in ``fred-core``:
18
+ - Both Knowledge Flow (slide vision enrichment) and the Agentic PPT-filler preview
19
+ need the exact same ``soffice`` invocation. Keeping one implementation here means
20
+ there is a single place to harden (timeout, error handling) instead of two drifting
21
+ ``subprocess.run`` calls.
22
+
23
+ Contract:
24
+ - The conversion is **best-effort**: any failure (missing ``soffice``, a non-zero exit,
25
+ a timeout, or no PDF produced) returns ``None`` (for the bytes helper) rather than
26
+ raising through the caller. Callers decide how to degrade — the preview pane, for
27
+ instance, still returns the ``.pptx`` when the PDF cannot be produced.
28
+ - The bytes helper (:func:`convert_pptx_bytes_to_pdf`) is **async and bounded**: it runs
29
+ ``soffice`` off the event loop via ``asyncio.to_thread`` with a timeout, so it is safe
30
+ to call from an async tool without stalling the turn.
31
+
32
+ ``soffice`` is expected to be installed in both backend images.
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ import asyncio
38
+ import logging
39
+ import shutil
40
+ import subprocess # nosec: controlled command arguments, shell=False
41
+ import tempfile
42
+ from pathlib import Path
43
+
44
+ logger = logging.getLogger(__name__)
45
+
46
+ # LibreOffice can occasionally hang (font server, first-run profile init). A bounded
47
+ # timeout keeps a stuck conversion from stalling an async agent turn indefinitely.
48
+ DEFAULT_PPTX_PDF_TIMEOUT_SECONDS = 60.0
49
+
50
+ # Same export filter the Knowledge Flow processor has used in production: embed standard
51
+ # fonts and pin the PDF version so the rendered deck is faithful across viewers.
52
+ _PDF_EXPORT_FILTER = "pdf:writer_pdf_Export:EmbedStandardFonts=True,SelectPdfVersion=1"
53
+
54
+
55
+ def convert_pptx_file_to_pdf(
56
+ pptx_path: Path,
57
+ timeout_seconds: float = DEFAULT_PPTX_PDF_TIMEOUT_SECONDS,
58
+ ) -> Path | None:
59
+ """Convert a ``.pptx`` file to a PDF next to it using headless LibreOffice.
60
+
61
+ Blocking; returns the produced ``.pdf`` path, or ``None`` when ``soffice`` is
62
+ missing, fails, times out, or produces no file. Prefer :func:`convert_pptx_bytes_to_pdf`
63
+ from async code — this variant exists for synchronous callers (e.g. the Knowledge Flow
64
+ slide renderer) that already own a temp directory.
65
+ """
66
+ pdf_path = pptx_path.with_suffix(".pdf")
67
+ soffice_path = shutil.which("soffice")
68
+ if not soffice_path:
69
+ logger.error(
70
+ "[PPTX2PDF] LibreOffice (soffice) is not installed or not in PATH."
71
+ )
72
+ return None
73
+
74
+ try:
75
+ subprocess.run( # nosec: controlled command arguments, shell=False
76
+ [
77
+ soffice_path,
78
+ "--headless",
79
+ "--nologo",
80
+ "--nofirststartwizard",
81
+ "--convert-to",
82
+ _PDF_EXPORT_FILTER,
83
+ "--outdir",
84
+ str(pptx_path.parent),
85
+ str(pptx_path),
86
+ ],
87
+ check=True,
88
+ stdout=subprocess.PIPE,
89
+ stderr=subprocess.PIPE,
90
+ timeout=timeout_seconds,
91
+ )
92
+ except subprocess.TimeoutExpired:
93
+ logger.error(
94
+ "[PPTX2PDF] LibreOffice conversion timed out after %.0fs.", timeout_seconds
95
+ )
96
+ return None
97
+ except subprocess.CalledProcessError as exc:
98
+ logger.error(
99
+ "[PPTX2PDF] LibreOffice conversion failed: %s",
100
+ exc.stderr.decode(errors="ignore") if exc.stderr else exc,
101
+ )
102
+ return None
103
+
104
+ if pdf_path.exists():
105
+ logger.info("[PPTX2PDF] Converted PPTX to PDF: %s", pdf_path)
106
+ return pdf_path
107
+
108
+ logger.warning("[PPTX2PDF] Conversion completed but PDF not found: %s", pdf_path)
109
+ return None
110
+
111
+
112
+ async def convert_pptx_bytes_to_pdf(
113
+ pptx_bytes: bytes,
114
+ timeout_seconds: float = DEFAULT_PPTX_PDF_TIMEOUT_SECONDS,
115
+ ) -> bytes | None:
116
+ """Convert in-memory ``.pptx`` bytes to PDF bytes, off the event loop and bounded.
117
+
118
+ Returns the PDF bytes, or ``None`` when the conversion is unavailable or fails (missing
119
+ ``soffice``, non-zero exit, timeout, or no PDF produced). Never raises for a conversion
120
+ problem, so a caller can treat the preview as best-effort.
121
+
122
+ Example:
123
+ ```python
124
+ pdf = await convert_pptx_bytes_to_pdf(filled_bytes)
125
+ if pdf is None:
126
+ ... # keep the .pptx, skip the preview
127
+ ```
128
+ """
129
+
130
+ def _run() -> bytes | None:
131
+ with tempfile.TemporaryDirectory(prefix="pptx2pdf-") as tmp:
132
+ pptx_path = Path(tmp) / "deck.pptx"
133
+ pptx_path.write_bytes(pptx_bytes)
134
+ pdf_path = convert_pptx_file_to_pdf(
135
+ pptx_path, timeout_seconds=timeout_seconds
136
+ )
137
+ if pdf_path is None:
138
+ return None
139
+ return pdf_path.read_bytes()
140
+
141
+ try:
142
+ return await asyncio.to_thread(_run)
143
+ except (
144
+ Exception
145
+ ): # pragma: no cover - defensive: never let conversion break the caller
146
+ logger.exception("[PPTX2PDF] Unexpected error during PPTX→PDF conversion.")
147
+ return None
@@ -17,12 +17,12 @@ import re
17
17
  import time
18
18
  from typing import List, Optional, Set
19
19
 
20
+ from fred_core.common.gcs_client import build_gcs_client
20
21
  from fred_core.filesystem.structures import (
21
22
  BaseFilesystem,
22
23
  FilesystemResourceInfo,
23
24
  FilesystemResourceInfoResult,
24
25
  )
25
- from google.cloud import storage
26
26
  from google.cloud.exceptions import NotFound
27
27
 
28
28
  logger = logging.getLogger(__name__)
@@ -67,9 +67,7 @@ class GcsFilesystem(BaseFilesystem):
67
67
  # Prefix injected externally by the app if needed (mirrors MinioFilesystem).
68
68
  self.prefix: str | None = None
69
69
 
70
- self.client = (
71
- storage.Client(project=project_id) if project_id else storage.Client()
72
- )
70
+ self.client = build_gcs_client(project_id)
73
71
  self.bucket = self.client.bucket(bucket_name)
74
72
 
75
73
  def health_check(self) -> dict:
@@ -222,12 +220,14 @@ class GcsFilesystem(BaseFilesystem):
222
220
  except NotFound:
223
221
  logger.warning("[GCS_DELETE] already gone: %s", blob.name)
224
222
 
225
- # Delete the object itself and a possible directory marker.
223
+ # Delete the object itself and a possible directory marker. The marker in
224
+ # particular may simply never have existed, so NotFound here is routine,
225
+ # not warning-worthy.
226
226
  for key in (resolved, prefix):
227
227
  try:
228
228
  self.bucket.blob(key).delete()
229
229
  except NotFound:
230
- pass
230
+ logger.debug("[GCS_DELETE] already gone: %s", key)
231
231
 
232
232
  async def print_root_dir(self) -> str:
233
233
  """Return the logical root URI in ``gs://bucket/prefix`` form."""
@@ -63,8 +63,17 @@ class Resource(str, Enum):
63
63
  ORGANIZATION = "organization"
64
64
 
65
65
 
66
- class AuthorizationError(Exception):
67
- """Raised when a user is not authorized to perform an action."""
66
+ class AuthorizationError(PermissionError):
67
+ """Raised when a user is not authorized to perform an action.
68
+
69
+ Inherits from the builtin `PermissionError` (not bare `Exception`) so that
70
+ every call site's `except PermissionError` — the standard ReBAC-denial ->
71
+ 403 mapping — also catches this without needing its own `except
72
+ AuthorizationError` clause. Without this, a denial raised via
73
+ `check_permission_or_raise` (which raises `AuthorizationError`) falls
74
+ through to a generic `except Exception` handler and surfaces as an
75
+ unhandled 500 instead of 403 wherever only `PermissionError` is caught.
76
+ """
68
77
 
69
78
  def __init__(
70
79
  self,
@@ -50,6 +50,15 @@ CLOCK_SKEW_SECONDS = int(os.getenv("FRED_JWT_CLOCK_SKEW", "0")) # optional leew
50
50
  JWT_CACHE_ENABLED = read_env_bool("FRED_JWT_CACHE_ENABLED", default=True)
51
51
  JWT_CACHE_TTL_SECONDS = int(os.getenv("FRED_JWT_CACHE_TTL", "60"))
52
52
  JWT_CACHE_MAX_SIZE = int(os.getenv("FRED_JWT_CACHE_SIZE", "512"))
53
+ # Application-side ceiling on token lifetime (exp - iat), independent of
54
+ # whatever an IdP was configured to issue. Unlike STRICT_ISSUER/STRICT_AUDIENCE
55
+ # (opt-in, gated behind the c3 profile, never enabled by any config in this
56
+ # repo), this stays on by default: a misconfigured or unfamiliar IdP issuing
57
+ # long-lived tokens should not be silently trusted. Fred's own M2M provider
58
+ # (`backend_to_backend_auth.py`) already mints short-lived, auto-refreshed
59
+ # tokens per call, so this does not affect normal service-to-service traffic
60
+ # — raise the env var if a deployment genuinely needs a longer ceiling.
61
+ MAX_TOKEN_LIFETIME_SECONDS = int(os.getenv("FRED_JWT_MAX_LIFETIME_SECONDS", "3600"))
53
62
 
54
63
  # Initialize global variables (to be set later)
55
64
  KEYCLOAK_ENABLED = False
@@ -404,6 +413,50 @@ def decode_jwt(token: str) -> KeycloakUser:
404
413
  headers={"WWW-Authenticate": "Bearer error='invalid_token'"},
405
414
  )
406
415
 
416
+ # Defense-in-depth ceiling on token lifetime, independent of the issuing
417
+ # IdP's own configuration (see MAX_TOKEN_LIFETIME_SECONDS docstring above).
418
+ # `verify_exp` above only rejects a token that has already expired; this
419
+ # additionally rejects one that was never supposed to live this long in
420
+ # the first place, regardless of whether it currently happens to still be
421
+ # valid.
422
+ iat, exp = payload.get("iat"), payload.get("exp")
423
+ if isinstance(iat, (int, float)) and isinstance(exp, (int, float)):
424
+ lifetime_seconds = exp - iat
425
+ if lifetime_seconds < 0:
426
+ # `verify_exp` above only checks exp against "now" — it does not
427
+ # relate exp to iat, so a token claiming iat after its own exp
428
+ # (malformed, or a confused/misconfigured IdP) can still pass it
429
+ # as long as exp itself is still in the future. That negative
430
+ # lifetime would otherwise fail the ">" ceiling check below
431
+ # silently instead of being rejected.
432
+ logger.warning(
433
+ "[AUTH] JWT has negative lifetime (exp before iat): iat=%s exp=%s sub=%s",
434
+ iat,
435
+ exp,
436
+ payload.get("sub"),
437
+ )
438
+ raise HTTPException(
439
+ status_code=401,
440
+ detail="Token has invalid iat/exp claims",
441
+ headers={
442
+ "WWW-Authenticate": "Bearer error='invalid_token', error_description='exp before iat'"
443
+ },
444
+ )
445
+ if lifetime_seconds > MAX_TOKEN_LIFETIME_SECONDS:
446
+ logger.warning(
447
+ "[AUTH] JWT lifetime exceeds policy ceiling: lifetime=%ss max=%ss sub=%s",
448
+ lifetime_seconds,
449
+ MAX_TOKEN_LIFETIME_SECONDS,
450
+ payload.get("sub"),
451
+ )
452
+ raise HTTPException(
453
+ status_code=401,
454
+ detail="Token lifetime exceeds the maximum permitted duration",
455
+ headers={
456
+ "WWW-Authenticate": "Bearer error='invalid_token', error_description='token lifetime too long'"
457
+ },
458
+ )
459
+
407
460
  # Extract client roles
408
461
  client_roles = []
409
462
  if "resource_access" in payload:
@@ -21,6 +21,7 @@ from dataclasses import dataclass
21
21
  from enum import Enum
22
22
  from typing import Iterable
23
23
 
24
+ from fred_core.common.team_id import is_personal_team_id, personal_team_id
24
25
  from fred_core.logs.audit_log import emit_audit_log
25
26
  from fred_core.security.models import AuthorizationError, Resource
26
27
  from fred_core.security.structure import KeycloakUser
@@ -166,9 +167,21 @@ class TeamPermission(str, Enum):
166
167
 
167
168
  # Team permissions a `service_agent` caller is allowed to satisfy without an OpenFGA
168
169
  # relation (RFC EVAL-AUTH, Solution A). Read-only: the evaluation worker executes
169
- # agents (gated by CAN_READ) and never mutates a team. Any write permission falls
170
- # through to the normal ReBAC check and is therefore denied.
171
- SERVICE_AGENT_ALLOWED_TEAM_PERMISSIONS = frozenset({TeamPermission.CAN_READ})
170
+ # agents and prepares their execution context, never mutates a team. Any write
171
+ # permission falls through to the normal ReBAC check and is therefore denied.
172
+ #
173
+ # CAN_USE_TEAM_AGENTS (2026-07-20, AGENT-EVALUATION-RFC.md §8.6): added after an
174
+ # AUTHZ-05 review moved `POST .../agent-instances/{id}/prepare-execution` from
175
+ # CAN_READ to CAN_USE_TEAM_AGENTS (real prompt content leak, product/api.py) without
176
+ # updating this allowlist to match — the evaluation worker's M2M identity, which
177
+ # structurally never holds a team relation, was then unconditionally denied on every
178
+ # run case. Safe to include: CAN_USE_TEAM_AGENTS gates read/prepare only, never a
179
+ # mutation, and the leak concern that moved prepare-execution off CAN_READ
180
+ # (`context_prompt_text`) never applies to the worker's calls — they never pass
181
+ # `session_id`, the only way that field is populated.
182
+ SERVICE_AGENT_ALLOWED_TEAM_PERMISSIONS = frozenset(
183
+ {TeamPermission.CAN_READ, TeamPermission.CAN_USE_TEAM_AGENTS}
184
+ )
172
185
 
173
186
 
174
187
  class AgentPermission(str, Enum):
@@ -346,6 +359,7 @@ class RebacEngine(ABC):
346
359
  - Save `team thales owner tag cir`.
347
360
  Returns a backend-specific consistency token when available.
348
361
  """
362
+ self._reject_unsanctioned_personal_team_write(relation)
349
363
  token = await self._persist_relation(relation)
350
364
  if self.enabled:
351
365
  emit_audit_log(
@@ -357,6 +371,48 @@ class RebacEngine(ABC):
357
371
  )
358
372
  return token
359
373
 
374
+ @staticmethod
375
+ def _reject_unsanctioned_personal_team_write(relation: Relation) -> None:
376
+ """Enforce the personal-team tuple invariant at the one audited write chokepoint.
377
+
378
+ A personal team (`personal-<uid>`) is a real ReBAC team object (AUTHZ-08),
379
+ but the *only* tuples ever allowed to name one are:
380
+ - the owner's own `team_editor` grant, self-healed by
381
+ `_ensure_personal_team_editor` on first touch;
382
+ - the structural `organization -> team` edge written by
383
+ `ensure_team_organization_relations` (same shape used for every team).
384
+
385
+ Every other shape — a different user, an elevated role, another relation
386
+ type — is refused here, unconditionally, regardless of caller. This is
387
+ what makes writing a real tuple for personal teams safe: no admin API,
388
+ import/export path, or future caller can ever grant (or be tricked into
389
+ granting) access to someone else's personal space, because there is
390
+ exactly one write chokepoint (`add_relation`) and it only recognizes
391
+ these two shapes for personal-team resources.
392
+ """
393
+ if relation.resource.type != Resource.TEAM or not is_personal_team_id(
394
+ relation.resource.id
395
+ ):
396
+ return
397
+ is_owner_editor_grant = (
398
+ relation.subject.type == Resource.USER
399
+ and relation.relation == RelationType.TEAM_EDITOR
400
+ and relation.resource.id == personal_team_id(relation.subject.id)
401
+ )
402
+ is_organization_edge = (
403
+ relation.subject.type == Resource.ORGANIZATION
404
+ and relation.subject.id == ORGANIZATION_ID
405
+ and relation.relation == RelationType.ORGANIZATION
406
+ )
407
+ if is_owner_editor_grant or is_organization_edge:
408
+ return
409
+ raise ValueError(
410
+ f"Refusing to write {relation.relation.value} for "
411
+ f"{relation.subject.type.value}:{relation.subject.id} onto personal "
412
+ f"team {relation.resource.id!r} — only the space's own owner may hold "
413
+ f"team_editor there, and it is provisioned automatically."
414
+ )
415
+
360
416
  @abstractmethod
361
417
  async def _persist_relation(self, relation: Relation) -> str | None:
362
418
  """Backend-specific write for one relation.
@@ -598,10 +654,21 @@ class RebacEngine(ABC):
598
654
  consistency_token: str | None = None,
599
655
  ) -> list[RebacReference] | RebacDisabledResult:
600
656
  """List resources a user can access for one permission."""
657
+ resource_type = _resource_for_permission(permission)
658
+ # Self-heal the caller's own personal-team tuple before enumerating
659
+ # (AUTHZ-08 follow-up): `lookup_resources`/OpenFGA `ListObjects` never
660
+ # goes through `has_user_permission`/`check_user_team_permission_or_raise`,
661
+ # so without this a first-touch user's own personal team was silently
662
+ # missing from "list my teams" results (e.g. the `/fs` virtual `/teams`
663
+ # directory) until some other check-triggering call happened to
664
+ # provision it first.
665
+ await self._ensure_personal_team_editor(
666
+ user, resource_type, personal_team_id(user.uid)
667
+ )
601
668
  return await self.lookup_resources(
602
669
  subject=RebacReference(Resource.USER, user.uid),
603
670
  permission=permission,
604
- resource_type=_resource_for_permission(permission),
671
+ resource_type=resource_type,
605
672
  consistency_token=consistency_token,
606
673
  )
607
674
 
@@ -617,6 +684,38 @@ class RebacEngine(ABC):
617
684
  ) -> bool:
618
685
  """Return `True` when a subject is authorized for an action."""
619
686
 
687
+ async def _ensure_personal_team_editor(
688
+ self, user: KeycloakUser, resource_type: Resource, resource_id: str
689
+ ) -> None:
690
+ """Self-heal a personal team's own `team_editor` tuple on first touch (AUTHZ-08).
691
+
692
+ Personal teams (`personal-<uid>`) are real ReBAC team objects, but carry
693
+ no tuple until the owner's first permission check touches one — this
694
+ lazily provisions exactly one (`user:<uid> team_editor team:personal-
695
+ <uid>`), matching `build_personal_team`'s hardcoded permission set
696
+ (`can_read`, `can_update_resources`, `can_update_agents` — all implied
697
+ by `team_editor`). Runs before every `check_user_permission_or_raise`/
698
+ `has_user_permission` call so it applies uniformly across every backend,
699
+ with no per-caller special-casing.
700
+
701
+ Never provisions for another user's personal team — `add_relation`'s own
702
+ write-guard would refuse that shape regardless, but this check avoids
703
+ even attempting (and audit-logging) a doomed write on every such call.
704
+ """
705
+ if not self.enabled or resource_type != Resource.TEAM:
706
+ return
707
+ if not is_personal_team_id(resource_id):
708
+ return
709
+ if resource_id != personal_team_id(user.uid):
710
+ return
711
+ team_ref = RebacReference(Resource.TEAM, resource_id)
712
+ user_ref = RebacReference(Resource.USER, user.uid)
713
+ if await self.has_direct_relation(user_ref, RelationType.TEAM_EDITOR, team_ref):
714
+ return
715
+ await self.add_user_relation(
716
+ user, RelationType.TEAM_EDITOR, Resource.TEAM, resource_id
717
+ )
718
+
620
719
  async def has_user_permission(
621
720
  self,
622
721
  user: KeycloakUser,
@@ -627,6 +726,7 @@ class RebacEngine(ABC):
627
726
  ) -> bool:
628
727
  """Check one permission for one user/resource pair."""
629
728
  resource_type = _resource_for_permission(permission)
729
+ await self._ensure_personal_team_editor(user, resource_type, resource_id)
630
730
  return await self.has_permission(
631
731
  RebacReference(Resource.USER, user.uid),
632
732
  permission,
@@ -680,6 +780,7 @@ class RebacEngine(ABC):
680
780
  ) -> None:
681
781
  """User-focused wrapper around `check_permission_or_raise`."""
682
782
  resource_type = _resource_for_permission(permission)
783
+ await self._ensure_personal_team_editor(user, resource_type, resource_id)
683
784
  await self.check_permission_or_raise(
684
785
  RebacReference(Resource.USER, user.uid),
685
786
  permission,
@@ -13,6 +13,7 @@
13
13
  # limitations under the License.
14
14
 
15
15
  from fred_core.store.base_content_store import ContentStore
16
+ from fred_core.store.gcs_content_store import GcsContentStore
16
17
  from fred_core.store.local_content_store import LocalContentStore
17
18
  from fred_core.store.minio_content_store import MinioContentStore
18
19
  from fred_core.store.opensearch_mapping_validator import (
@@ -23,6 +24,7 @@ from fred_core.store.vector_search import VectorSearchHit
23
24
 
24
25
  __all__ = [
25
26
  "ContentStore",
27
+ "GcsContentStore",
26
28
  "LocalContentStore",
27
29
  "MappingValidationError",
28
30
  "MinioContentStore",