howler-api 3.3.0.dev767__tar.gz → 3.3.0.dev773__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 (201) hide show
  1. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/PKG-INFO +4 -4
  2. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/v1/user.py +2 -4
  3. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/common/classification.py +162 -82
  4. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/config.py +1 -1
  5. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/helper/azure.py +7 -2
  6. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/helper/oauth.py +40 -16
  7. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/helper/search.py +1 -1
  8. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/config.py +1 -1
  9. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/hit.py +7 -0
  10. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/user.py +1 -0
  11. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/random_data.py +14 -1
  12. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/randomizer.py +6 -4
  13. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/services/jwt_service.py +2 -2
  14. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/services/user_service.py +38 -14
  15. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/pyproject.toml +7 -7
  16. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/README.md +0 -0
  17. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/__init__.py +0 -0
  18. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/actions/__init__.py +0 -0
  19. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/actions/add_label.py +0 -0
  20. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/actions/add_to_bundle.py +0 -0
  21. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/actions/change_field.py +0 -0
  22. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/actions/demote.py +0 -0
  23. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/actions/example_plugin.py +0 -0
  24. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/actions/prioritization.py +0 -0
  25. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/actions/promote.py +0 -0
  26. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/actions/remove_from_bundle.py +0 -0
  27. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/actions/remove_label.py +0 -0
  28. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/actions/transition.py +0 -0
  29. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/__init__.py +0 -0
  30. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/base.py +0 -0
  31. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/socket.py +0 -0
  32. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/v1/__init__.py +0 -0
  33. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/v1/action.py +0 -0
  34. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/v1/analytic.py +0 -0
  35. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/v1/auth.py +0 -0
  36. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/v1/clue.py +0 -0
  37. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/v1/configs.py +0 -0
  38. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/v1/dossier.py +0 -0
  39. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/v1/help.py +0 -0
  40. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/v1/hit.py +0 -0
  41. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/v1/notebook.py +0 -0
  42. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/v1/overview.py +0 -0
  43. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/v1/search.py +0 -0
  44. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/v1/template.py +0 -0
  45. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/v1/tool.py +0 -0
  46. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/v1/utils/__init__.py +0 -0
  47. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/v1/utils/etag.py +0 -0
  48. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/api/v1/view.py +0 -0
  49. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/app.py +0 -0
  50. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/common/README.md +0 -0
  51. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/common/__init__.py +0 -0
  52. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/common/classification.yml +0 -0
  53. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/common/exceptions.py +0 -0
  54. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/common/loader.py +0 -0
  55. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/common/logging/__init__.py +0 -0
  56. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/common/logging/audit.py +0 -0
  57. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/common/logging/format.py +0 -0
  58. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/common/net.py +0 -0
  59. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/common/net_static.py +0 -0
  60. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/common/random_user.py +0 -0
  61. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/common/swagger.py +0 -0
  62. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/cronjobs/__init__.py +0 -0
  63. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/cronjobs/retention.py +0 -0
  64. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/cronjobs/rules.py +0 -0
  65. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/cronjobs/view_cleanup.py +0 -0
  66. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/datastore/README.md +0 -0
  67. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/datastore/__init__.py +0 -0
  68. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/datastore/bulk.py +0 -0
  69. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/datastore/collection.py +0 -0
  70. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/datastore/constants.py +0 -0
  71. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/datastore/exceptions.py +0 -0
  72. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/datastore/howler_store.py +0 -0
  73. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/datastore/migrations/fix_process.py +0 -0
  74. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/datastore/operations.py +0 -0
  75. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/datastore/schemas.py +0 -0
  76. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/datastore/store.py +0 -0
  77. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/datastore/support/__init__.py +0 -0
  78. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/datastore/support/build.py +0 -0
  79. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/datastore/support/schemas.py +0 -0
  80. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/datastore/types.py +0 -0
  81. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/error.py +0 -0
  82. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/external/README.md +0 -0
  83. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/external/__init__.py +0 -0
  84. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/external/generate_mitre.py +0 -0
  85. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/external/generate_sigma_rules.py +0 -0
  86. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/external/generate_tlds.py +0 -0
  87. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/external/reindex_data.py +0 -0
  88. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/external/wipe_databases.py +0 -0
  89. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/gunicorn_config.py +0 -0
  90. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/healthz.py +0 -0
  91. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/helper/__init__.py +0 -0
  92. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/helper/discover.py +0 -0
  93. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/helper/hit.py +0 -0
  94. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/helper/workflow.py +0 -0
  95. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/helper/ws.py +0 -0
  96. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/README.md +0 -0
  97. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/__init__.py +0 -0
  98. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/base.py +0 -0
  99. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/charter.txt +0 -0
  100. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/helper.py +0 -0
  101. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/howler_enum.py +0 -0
  102. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/__init__.py +0 -0
  103. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/action.py +0 -0
  104. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/analytic.py +0 -0
  105. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/assemblyline.py +0 -0
  106. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/aws.py +0 -0
  107. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/azure.py +0 -0
  108. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/cbs.py +0 -0
  109. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/clue.py +0 -0
  110. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/dossier.py +0 -0
  111. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/__init__.py +0 -0
  112. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/agent.py +0 -0
  113. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/autonomous_system.py +0 -0
  114. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/client.py +0 -0
  115. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/cloud.py +0 -0
  116. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/code_signature.py +0 -0
  117. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/container.py +0 -0
  118. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/dns.py +0 -0
  119. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/egress.py +0 -0
  120. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/elf.py +0 -0
  121. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/email.py +0 -0
  122. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/error.py +0 -0
  123. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/event.py +0 -0
  124. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/faas.py +0 -0
  125. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/file.py +0 -0
  126. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/geo.py +0 -0
  127. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/group.py +0 -0
  128. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/hash.py +0 -0
  129. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/host.py +0 -0
  130. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/http.py +0 -0
  131. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/ingress.py +0 -0
  132. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/interface.py +0 -0
  133. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/network.py +0 -0
  134. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/observer.py +0 -0
  135. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/organization.py +0 -0
  136. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/os.py +0 -0
  137. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/pe.py +0 -0
  138. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/process.py +0 -0
  139. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/registry.py +0 -0
  140. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/related.py +0 -0
  141. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/rule.py +0 -0
  142. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/server.py +0 -0
  143. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/threat.py +0 -0
  144. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/tls.py +0 -0
  145. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/url.py +0 -0
  146. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/user.py +0 -0
  147. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/user_agent.py +0 -0
  148. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/ecs/vulnerability.py +0 -0
  149. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/gcp.py +0 -0
  150. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/howler_data.py +0 -0
  151. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/lead.py +0 -0
  152. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/localized_label.py +0 -0
  153. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/overview.py +0 -0
  154. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/pivot.py +0 -0
  155. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/template.py +0 -0
  156. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/odm/models/view.py +0 -0
  157. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/patched.py +0 -0
  158. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/plugins/__init__.py +0 -0
  159. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/plugins/config.py +0 -0
  160. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/remote/__init__.py +0 -0
  161. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/remote/datatypes/README.md +0 -0
  162. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/remote/datatypes/__init__.py +0 -0
  163. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/remote/datatypes/counters.py +0 -0
  164. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/remote/datatypes/events.py +0 -0
  165. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/remote/datatypes/hash.py +0 -0
  166. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/remote/datatypes/lock.py +0 -0
  167. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/remote/datatypes/queues/__init__.py +0 -0
  168. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/remote/datatypes/queues/comms.py +0 -0
  169. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/remote/datatypes/queues/multi.py +0 -0
  170. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/remote/datatypes/queues/named.py +0 -0
  171. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/remote/datatypes/queues/priority.py +0 -0
  172. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/remote/datatypes/set.py +0 -0
  173. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/remote/datatypes/user_quota_tracker.py +0 -0
  174. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/security/__init__.py +0 -0
  175. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/security/socket.py +0 -0
  176. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/security/utils.py +0 -0
  177. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/services/__init__.py +0 -0
  178. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/services/action_service.py +0 -0
  179. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/services/analytic_service.py +0 -0
  180. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/services/auth_service.py +0 -0
  181. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/services/config_service.py +0 -0
  182. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/services/dossier_service.py +0 -0
  183. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/services/event_service.py +0 -0
  184. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/services/hit_service.py +0 -0
  185. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/services/lucene_service.py +0 -0
  186. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/services/notebook_service.py +0 -0
  187. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/services/overview_service.py +0 -0
  188. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/services/template_service.py +0 -0
  189. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/telemetry.py +0 -0
  190. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/utils/__init__.py +0 -0
  191. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/utils/annotations.py +0 -0
  192. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/utils/chunk.py +0 -0
  193. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/utils/constants.py +0 -0
  194. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/utils/dict_utils.py +0 -0
  195. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/utils/isotime.py +0 -0
  196. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/utils/list_utils.py +0 -0
  197. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/utils/lucene.py +0 -0
  198. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/utils/path.py +0 -0
  199. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/utils/socket_utils.py +0 -0
  200. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/utils/str_utils.py +0 -0
  201. {howler_api-3.3.0.dev767 → howler_api-3.3.0.dev773}/howler/utils/uid.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: howler-api
3
- Version: 3.3.0.dev767
3
+ Version: 3.3.0.dev773
4
4
  Summary: Howler - API server
5
5
  License: MIT
6
6
  Keywords: howler,alerting,gc,canada,cse-cst,cse,cst,cyber,cccs
@@ -38,7 +38,7 @@ Requires-Dist: opentelemetry-instrumentation-flask (==0.61b0)
38
38
  Requires-Dist: opentelemetry-sdk (==1.40.0)
39
39
  Requires-Dist: packaging (<25.0)
40
40
  Requires-Dist: passlib (==1.7.4)
41
- Requires-Dist: prometheus-client (==0.24.1)
41
+ Requires-Dist: prometheus-client (==0.25.0)
42
42
  Requires-Dist: pydantic (>=2.11.4,<3.0.0)
43
43
  Requires-Dist: pydantic-settings[yaml] (>=2.9.1,<3.0.0)
44
44
  Requires-Dist: pydash (>=8.0.5,<9.0.0)
@@ -51,10 +51,10 @@ Requires-Dist: python-dotenv (>=1.1.0,<2.0.0)
51
51
  Requires-Dist: pytz (>=2025.2,<2026.0)
52
52
  Requires-Dist: pyyaml (==6.0.3)
53
53
  Requires-Dist: redis (==4.6.0)
54
- Requires-Dist: requests (==2.33.0)
54
+ Requires-Dist: requests (==2.33.1)
55
55
  Requires-Dist: tzdata (>=2026.1,<2027.0)
56
56
  Requires-Dist: validators (>=0.34,<0.36)
57
- Requires-Dist: wsproto (==1.2.0)
57
+ Requires-Dist: wsproto (==1.3.2)
58
58
  Project-URL: Documentation, https://cybercentrecanada.github.io/howler/developer/backend/
59
59
  Project-URL: Homepage, https://cybercentrecanada.github.io/howler/
60
60
  Project-URL: Repository, https://github.com/CybercentreCanada/howler-api
@@ -128,9 +128,7 @@ def add_user_account(username, **_):
128
128
  data["name"] = data["uname"]
129
129
 
130
130
  # Add dynamic classification group
131
- data["classification"] = user_service.get_dynamic_classification(
132
- cast(str | None, data.get("classification", None)), data["email"]
133
- )
131
+ data["classification"] = user_service.get_dynamic_classification(data)
134
132
 
135
133
  # Clear non user account data
136
134
  avatar = data.pop("avatar", None)
@@ -290,7 +288,7 @@ def set_user_account(username: str, **kwargs): # noqa: C901
290
288
  data.pop("new_pass_confirm", None)
291
289
 
292
290
  # Apply dynamic classification
293
- data["classification"] = user_service.get_dynamic_classification(data["classification"], data["email"])
291
+ data["classification"] = user_service.get_dynamic_classification(data)
294
292
 
295
293
  ret_val = user_service.save_user_account(username, data, kwargs["user"])
296
294
  return ok({"success": ret_val})
@@ -38,7 +38,7 @@ class Classification(object):
38
38
  "description",
39
39
  ]
40
40
  self.original_definition = classification_definition
41
- self.levels_map: dict[str, str] = {}
41
+ self.levels_map: dict[str, int] = {}
42
42
  self.levels_map_stl = {}
43
43
  self.levels_map_lts = {}
44
44
  self.levels_styles_map = {}
@@ -64,6 +64,9 @@ class Classification(object):
64
64
 
65
65
  self.enforce = False
66
66
  self.dynamic_groups = False
67
+ # dynamic group type is one of: email | group | all
68
+ # defaults to email for original behavior
69
+ self.dynamic_groups_type = "email"
67
70
 
68
71
  # Add Invalid classification
69
72
  self.levels_map["INV"] = self.INVALID_LVL
@@ -84,12 +87,10 @@ class Classification(object):
84
87
  raise HowlerKeyError("Enforce not set!")
85
88
 
86
89
  self.dynamic_groups = classification_definition.get("dynamic_groups", None)
87
- if self.enforce is None:
90
+ if self.dynamic_groups is None:
88
91
  raise HowlerKeyError("Dynamic groups not set!")
89
92
 
90
- if self.enforce:
91
- self._classification_cache = self.list_all_classification_combinations()
92
- self._classification_cache_short = self.list_all_classification_combinations(long_format=False)
93
+ self.dynamic_groups_type = classification_definition.get("dynamic_groups_type", self.dynamic_groups_type)
93
94
 
94
95
  if classification_definition.get("levels", None) is None:
95
96
  raise HowlerKeyError("No classification levels provided!")
@@ -186,6 +187,16 @@ class Classification(object):
186
187
  self.description[short_name] = x.get("description", "N/A")
187
188
  self.description[name] = self.description[short_name]
188
189
 
190
+ # Build the classification cache AFTER all maps are populated so
191
+ # that normalize_classification can fully validate each combination.
192
+ # Using normalized=True filters out invalid combos (e.g. subgroups
193
+ # that violate limited_to_group constraints).
194
+ if self.enforce:
195
+ self._classification_cache = self.list_all_classification_combinations(normalized=True)
196
+ self._classification_cache_short = self.list_all_classification_combinations(
197
+ long_format=False, normalized=True
198
+ )
199
+
189
200
  if not self.is_valid(classification_definition["unrestricted"]):
190
201
  raise InvalidDefinition("Classification definition's unrestricted classification is invalid.")
191
202
 
@@ -238,40 +249,46 @@ class Classification(object):
238
249
 
239
250
  return items
240
251
 
241
- def _get_c12n_level_index(self, c12n: str) -> str:
252
+ def _get_c12n_level_index(self, c12n: str) -> tuple[int, str]:
242
253
  # Parse classifications in uppercase mode only
243
254
  c12n = c12n.upper()
244
255
 
245
- lvl = c12n.split("//")[0]
256
+ lvl, _, remain = c12n.partition("//")
246
257
  if lvl in self.levels_map:
247
- return self.levels_map[lvl]
258
+ return self.levels_map[lvl], remain
248
259
  elif lvl in self.levels_map_lts:
249
- return self.levels_map[self.levels_map_lts[lvl]]
260
+ return self.levels_map[self.levels_map_lts[lvl]], remain
250
261
  elif lvl in self.levels_aliases:
251
- return self.levels_map[self.levels_aliases[lvl]]
262
+ return self.levels_map[self.levels_aliases[lvl]], remain
252
263
  else:
253
264
  raise InvalidClassification(
254
265
  "Classification level '%s' was not found in your classification definition." % lvl
255
266
  )
256
267
 
257
268
  def _get_c12n_level_text(self, lvl_idx: int, long_format: bool = True) -> str:
258
- text = self.levels_map.get(str(lvl_idx), None)
269
+ text: Any = self.levels_map.get(str(lvl_idx), None)
270
+
259
271
  if not text:
260
272
  raise InvalidClassification(
261
273
  "Classification level number '%s' was not found in your classification definition." % lvl_idx
262
274
  )
275
+
263
276
  if long_format:
264
277
  return self.levels_map_stl[text]
278
+
265
279
  return text
266
280
 
267
- def _get_c12n_required(self, c12n: str, long_format: bool = True) -> List:
281
+ def _get_c12n_required(self, c12n: str, long_format: bool = True) -> tuple[List, list[str]]:
268
282
  # Parse classifications in uppercase mode only
269
283
  c12n = c12n.upper()
270
284
 
271
285
  return_set = set()
272
286
  part_set = set(c12n.split("/"))
287
+ unused = []
273
288
 
274
289
  for p in part_set:
290
+ if not p:
291
+ continue
275
292
  if p in self.access_req_map_lts:
276
293
  return_set.add(self.access_req_map_lts[p])
277
294
  elif p in self.access_req_map_stl:
@@ -279,27 +296,37 @@ class Classification(object):
279
296
  elif p in self.access_req_aliases:
280
297
  for a in self.access_req_aliases[p]:
281
298
  return_set.add(a)
299
+ else:
300
+ unused.append(p)
282
301
 
283
302
  if long_format:
284
- return sorted([self.access_req_map_stl[r] for r in return_set])
285
- return sorted(list(return_set))
303
+ return sorted([self.access_req_map_stl[r] for r in return_set]), unused
304
+ return sorted(list(return_set)), unused
286
305
 
287
- def _get_c12n_groups(self, c12n: str, long_format: bool = True) -> Tuple[List, List]:
306
+ def _get_c12n_groups(
307
+ self, c12n: list[str], long_format: bool = True, get_dynamic_groups: bool = True, auto_select: bool = False
308
+ ) -> Tuple[List, List, list[str]]:
288
309
  # Parse classifications in uppercase mode only
289
- c12n = c12n.upper()
290
310
 
291
311
  g1_set = set()
292
312
  g2_set = set()
293
313
  others = set()
294
314
 
295
- grp_part = c12n.split("//")
296
315
  groups = []
297
- for gp in grp_part:
298
- gp = gp.replace("REL TO ", "")
299
- gp = gp.replace("REL ", "")
300
- temp_group = set([x.strip() for x in gp.split(",")])
301
- for t in temp_group:
302
- groups.extend(t.split("/"))
316
+ subgroups = []
317
+
318
+ for gp in c12n:
319
+ # If there is a rel marking we know we have groups
320
+ if gp.startswith("REL "):
321
+ gp = gp.replace("REL TO ", "")
322
+ gp = gp.replace("REL ", "")
323
+ temp_group = set([x.strip() for x in gp.split(",")])
324
+ for t in temp_group:
325
+ groups.extend(t.split("/"))
326
+ else:
327
+ # if there is not a rel marking we either have a subgroup or a solitary_display_name
328
+ # alias for a group, which we will filter out later
329
+ subgroups.append(gp)
303
330
 
304
331
  for g in groups:
305
332
  if g in self.groups_map_lts:
@@ -309,33 +336,63 @@ class Classification(object):
309
336
  elif g in self.groups_aliases:
310
337
  for a in self.groups_aliases[g]:
311
338
  g1_set.add(a)
312
- elif g in self.subgroups_map_lts:
339
+ else:
340
+ others.add(g)
341
+
342
+ for g in subgroups:
343
+ if g in self.subgroups_map_lts:
313
344
  g2_set.add(self.subgroups_map_lts[g])
314
345
  elif g in self.subgroups_map_stl:
315
346
  g2_set.add(g)
316
347
  elif g in self.subgroups_aliases:
317
348
  for a in self.subgroups_aliases[g]:
318
349
  g2_set.add(a)
350
+ # Here is where we catch any solitary_display_name aliases for groups within the subgroup sections
351
+ elif g in self.groups_aliases:
352
+ # Check that this alias is actually a solitary name, don't
353
+ # let other aliases leak outside the REL marking
354
+ groups = self.groups_aliases[g]
355
+ if len(groups) > 1:
356
+ raise InvalidClassification(f"Unclear use of alias: {g}")
357
+ g1_set.add(groups[0])
319
358
  else:
320
- others.add(g)
359
+ raise InvalidClassification(f"Unknown Subgroup: {g}")
360
+
361
+ # If dynamic groups are active all remaining parts should be groups found under a
362
+ # REL TO marking that we can merge in with the other groups
363
+ if self.dynamic_groups and get_dynamic_groups:
364
+ g1_set.update(others)
365
+ others = set()
366
+
367
+ # Check if there are any required group assignments
368
+ for subgroup in g2_set:
369
+ required = self.params_map.get(subgroup, {}).get("require_group", None)
370
+ if required:
371
+ g1_set.add(required)
372
+
373
+ # Check if there are any forbidden group assignments
374
+ for subgroup in g2_set:
375
+ limited_to_group = self.params_map.get(subgroup, {}).get("limited_to_group", None)
376
+ if limited_to_group is not None:
377
+ if len(g1_set) > 1 or (len(g1_set) == 1 and g1_set != set([limited_to_group])):
378
+ raise InvalidClassification(
379
+ f"Subgroup {subgroup} is limited to group {limited_to_group} (found: {', '.join(g1_set)})"
380
+ )
321
381
 
322
- if self.dynamic_groups:
323
- for o in others:
324
- if (
325
- o not in self.access_req_map_lts
326
- and o not in self.access_req_map_stl
327
- and o not in self.access_req_aliases
328
- and o not in self.levels_map
329
- and o not in self.levels_map_lts
330
- and o not in self.levels_aliases
331
- ):
332
- g1_set.add(o)
382
+ # Do auto select
383
+ if auto_select and g1_set:
384
+ g1_set.update(self.groups_auto_select_short)
385
+ if auto_select and g2_set:
386
+ g2_set.update(self.subgroups_auto_select_short)
333
387
 
388
+ # Swap to long format if required
334
389
  if long_format:
335
- return sorted([self.groups_map_stl.get(r, r) for r in g1_set]), sorted(
336
- [self.subgroups_map_stl[r] for r in g2_set]
390
+ return (
391
+ sorted([self.groups_map_stl.get(r, r) for r in g1_set]),
392
+ sorted([self.subgroups_map_stl[r] for r in g2_set]),
393
+ list(others),
337
394
  )
338
- return sorted(list(g1_set)), sorted(list(g2_set))
395
+ return sorted(list(g1_set)), sorted(list(g2_set)), list(others)
339
396
 
340
397
  @staticmethod
341
398
  def _can_see_required(user_req: List, req: List) -> bool:
@@ -446,11 +503,22 @@ class Classification(object):
446
503
  return out
447
504
 
448
505
  def _get_classification_parts(
449
- self, c12n: str, long_format: bool = True
450
- ) -> Tuple[Union[Union[int, str], Any], List, List, List]:
451
- lvl_idx = self._get_c12n_level_index(c12n)
452
- req = self._get_c12n_required(c12n, long_format=long_format)
453
- groups, subgroups = self._get_c12n_groups(c12n, long_format=long_format)
506
+ self,
507
+ c12n: str,
508
+ long_format: bool = True,
509
+ get_dynamic_groups: bool = True,
510
+ auto_select: bool = False,
511
+ ignore_unused: bool = False,
512
+ ) -> Tuple[int, list[str], list[str], list[str]]:
513
+ lvl_idx, unused = self._get_c12n_level_index(c12n)
514
+ req, unused_parts = self._get_c12n_required(unused, long_format=long_format)
515
+
516
+ groups, subgroups, unused_parts = self._get_c12n_groups(
517
+ unused_parts, long_format=long_format, get_dynamic_groups=get_dynamic_groups, auto_select=auto_select
518
+ )
519
+
520
+ if unused_parts and not ignore_unused:
521
+ raise InvalidClassification(f"Unparsable classification parts: {', '.join(unused_parts)}")
454
522
 
455
523
  return lvl_idx, req, groups, subgroups
456
524
 
@@ -473,7 +541,7 @@ class Classification(object):
473
541
  # Public functions
474
542
  # ++++++++++++++++++++++++
475
543
  # noinspection PyUnusedLocal
476
- def list_all_classification_combinations(self, long_format: bool = True) -> Set:
544
+ def list_all_classification_combinations(self, long_format: bool = True, normalized: bool = False) -> Set:
477
545
  combinations = set()
478
546
 
479
547
  levels = self._list_items_and_aliases(self.original_definition["levels"], long_format=long_format)
@@ -529,7 +597,10 @@ class Classification(object):
529
597
 
530
598
  temp_combinations = copy(combinations)
531
599
  for p in itertools.product(temp_combinations, sgrp_cbs):
532
- if "//REL TO " in p[0]:
600
+ # A combo already has a group part when it contains "//REL TO " (explicit)
601
+ # or ends with a solitary display name like "//ANY" (after replacement).
602
+ has_group = "//REL TO " in p[0] or any(p[0].endswith(f"//{sol_name}") for sol_name in solitary_names)
603
+ if has_group:
533
604
  cl = "/".join(p)
534
605
 
535
606
  if cl.endswith("/"):
@@ -537,13 +608,25 @@ class Classification(object):
537
608
  else:
538
609
  combinations.add(cl)
539
610
  else:
540
- cl = "//REL TO ".join(p)
611
+ # No group present — subgroups are joined with "//" (not "//REL TO ")
612
+ # to match the format produced by _get_normalized_classification_text.
613
+ # "REL TO" is reserved for groups in _get_c12n_groups.
614
+ cl = "//".join(p)
541
615
 
542
- if cl.endswith("//REL TO "):
543
- combinations.add(cl[:-9])
616
+ if cl.endswith("//"):
617
+ combinations.add(cl[:-2])
544
618
  else:
545
619
  combinations.add(cl)
546
620
 
621
+ if normalized:
622
+ good = []
623
+ for x in combinations:
624
+ try:
625
+ good.append(self.normalize_classification(x, long_format=long_format))
626
+ except InvalidClassification:
627
+ pass
628
+ return set(good)
629
+
547
630
  return combinations
548
631
 
549
632
  # noinspection PyUnusedLocal
@@ -581,7 +664,8 @@ class Classification(object):
581
664
  return out
582
665
 
583
666
  def get_access_control_parts(self, c12n: str, user_classification: bool = False) -> Dict:
584
- """Returns a dictionary containing the different access parameters Lucene needs to build it's queries
667
+ """
668
+ Returns a dictionary containing the different access parameters Lucene needs to build it's queries
585
669
 
586
670
  Args:
587
671
  c12n: The classification to get the parts from
@@ -591,12 +675,8 @@ class Classification(object):
591
675
  c12n = self.UNRESTRICTED
592
676
 
593
677
  try:
594
- # Normalize the classification before gathering the parts
595
- c12n = self.normalize_classification(c12n, skip_auto_select=user_classification)
596
-
597
- access_lvl = self._get_c12n_level_index(c12n)
598
- access_req = self._get_c12n_required(c12n, long_format=False)
599
- access_grp1, access_grp2 = self._get_c12n_groups(c12n, long_format=False)
678
+ parts = self._get_classification_parts(c12n, long_format=False, auto_select=not user_classification)
679
+ access_lvl, access_req, access_grp1, access_grp2 = parts
600
680
 
601
681
  return {
602
682
  "__access_lvl__": access_lvl,
@@ -679,7 +759,8 @@ class Classification(object):
679
759
  )
680
760
 
681
761
  def is_accessible(self, user_c12n: str, c12n: str, ignore_invalid: bool = False) -> bool:
682
- """Given a user classification, check if a user is allow to see a certain classification
762
+ """
763
+ Given a user classification, check if a user is allow to see a certain classification
683
764
 
684
765
  Args:
685
766
  user_c12n: Maximum classification for the user
@@ -698,16 +779,10 @@ class Classification(object):
698
779
  return True
699
780
 
700
781
  try:
701
- # Normalize classifications before comparing them
702
- user_c12n = self.normalize_classification(user_c12n, skip_auto_select=True)
703
- c12n = self.normalize_classification(c12n, skip_auto_select=True)
782
+ user_lvl, user_req, user_groups, user_subgroups = self._get_classification_parts(user_c12n)
783
+ lvl, req, groups, subgroups = self._get_classification_parts(c12n)
704
784
 
705
- user_req = self._get_c12n_required(user_c12n)
706
- user_groups, user_subgroups = self._get_c12n_groups(user_c12n)
707
- req = self._get_c12n_required(c12n)
708
- groups, subgroups = self._get_c12n_groups(c12n)
709
-
710
- if self._get_c12n_level_index(user_c12n) >= self._get_c12n_level_index(c12n):
785
+ if int(user_lvl) >= int(lvl):
711
786
  if not self._can_see_required(user_req, req):
712
787
  return False
713
788
  if not self._can_see_groups(user_groups, groups):
@@ -812,7 +887,7 @@ class Classification(object):
812
887
 
813
888
  return True
814
889
 
815
- def max_classification(self, c12n_1: str, c12n_2: str, long_format: bool = True) -> str:
890
+ def max_classification(self, c12n_1: str | None, c12n_2: str | None, long_format: bool = True) -> str:
816
891
  """Mixes to classification and returns to most restrictive form for them
817
892
 
818
893
  Args:
@@ -833,9 +908,10 @@ class Classification(object):
833
908
  c12n_2 = self.normalize_classification(c12n_2)
834
909
 
835
910
  if c12n_1 is None:
836
- return c12n_2
911
+ return c12n_2 if c12n_2 else self.UNRESTRICTED
912
+
837
913
  if c12n_2 is None:
838
- return c12n_1
914
+ return c12n_1 if c12n_1 else self.UNRESTRICTED
839
915
 
840
916
  lvl_idx_1, req_1, groups_1, subgroups_1 = self._get_classification_parts(c12n_1, long_format=long_format)
841
917
  lvl_idx_2, req_2, groups_2, subgroups_2 = self._get_classification_parts(c12n_2, long_format=long_format)
@@ -899,8 +975,16 @@ class Classification(object):
899
975
  long_format=long_format, # type: ignore
900
976
  )
901
977
 
902
- def normalize_classification(self, c12n: str, long_format: bool = True, skip_auto_select: bool = False) -> str:
903
- """Normalize a given classification by applying the rules defined in the classification definition.
978
+ def normalize_classification(
979
+ self,
980
+ c12n: str,
981
+ long_format: bool = True,
982
+ skip_auto_select: bool = False,
983
+ get_dynamic_groups: bool = True,
984
+ ignore_unused: bool = False,
985
+ ) -> str:
986
+ """
987
+ Normalize a given classification by applying the rules defined in the classification definition.
904
988
  This function will remove any invalid parts and add missing parts to the classification.
905
989
  It will also ensure that the display of the classification is always done the same way
906
990
 
@@ -916,19 +1000,16 @@ class Classification(object):
916
1000
  return self.UNRESTRICTED
917
1001
 
918
1002
  # Has the classification has already been normalized before?
919
- if long_format and c12n in self._classification_cache:
1003
+ if long_format and c12n in self._classification_cache and get_dynamic_groups:
920
1004
  return c12n
921
- if not long_format and c12n in self._classification_cache_short:
1005
+ if not long_format and c12n in self._classification_cache_short and get_dynamic_groups:
922
1006
  return c12n
923
1007
 
924
- lvl_idx, req, groups, subgroups = self._get_classification_parts(c12n, long_format=long_format)
1008
+ lvl_idx, req, groups, subgroups = self._get_classification_parts(
1009
+ c12n, long_format=long_format, get_dynamic_groups=get_dynamic_groups, ignore_unused=ignore_unused
1010
+ )
925
1011
  new_c12n = self._get_normalized_classification_text(
926
- lvl_idx, # type: ignore
927
- req,
928
- groups,
929
- subgroups,
930
- long_format=long_format,
931
- skip_auto_select=skip_auto_select,
1012
+ lvl_idx, req, groups, subgroups, long_format=long_format, skip_auto_select=skip_auto_select
932
1013
  )
933
1014
  if long_format:
934
1015
  self._classification_cache.add(new_c12n)
@@ -937,7 +1018,7 @@ class Classification(object):
937
1018
 
938
1019
  return new_c12n
939
1020
 
940
- def build_user_classification(self, c12n_1: str | None, c12n_2: str | None, long_format: bool = True) -> str | None:
1021
+ def build_user_classification(self, c12n_1: str, c12n_2: str, long_format: bool = True) -> str:
941
1022
  """Mixes to classification and return the classification marking that would give access to the most data
942
1023
 
943
1024
  Args:
@@ -959,7 +1040,6 @@ class Classification(object):
959
1040
 
960
1041
  if c12n_1 is None:
961
1042
  return c12n_2
962
-
963
1043
  if c12n_2 is None:
964
1044
  return c12n_1
965
1045
 
@@ -10,7 +10,7 @@ from howler.remote.datatypes.user_quota_tracker import UserQuotaTracker
10
10
  #################################################################
11
11
  # Configuration
12
12
 
13
- CLASSIFICATION = loader.get_classification()
13
+ CLASSIFICATION = loader.get_classification(os.getenv("HWL_CLASSIFICATION_PATH", None))
14
14
 
15
15
  AUDIT = config.ui.audit
16
16
 
@@ -1,6 +1,6 @@
1
1
  import requests
2
2
 
3
- from howler.common.exceptions import HowlerException
3
+ from howler.common.exceptions import HowlerAttributeError, HowlerException
4
4
  from howler.common.logging import get_logger
5
5
  from howler.config import config
6
6
  from howler.utils.str_utils import default_string_value
@@ -20,7 +20,12 @@ def azure_obo(token: str) -> str:
20
20
  Returns:
21
21
  str: The new access token with updated privileges
22
22
  """
23
- azure_provider_config = config.auth.oauth.providers["azure"]
23
+ if "azure" in config.auth.oauth.providers:
24
+ azure_provider_config = config.auth.oauth.providers["azure"]
25
+ elif "entraid" in config.auth.oauth.providers:
26
+ azure_provider_config = config.auth.oauth.providers["entraid"]
27
+ else:
28
+ raise HowlerAttributeError("No azure/entraid-based provider configured!")
24
29
 
25
30
  logger.debug("OBOing to MS Graph")
26
31
  # Azure is a special case here, as we need to OBO to MS Graph
@@ -60,7 +60,7 @@ def parse_profile(profile: dict[str, Any], provider_config: OAuthProvider) -> di
60
60
  uname = profile.get("uname", profile.get("preferred_username", email_adr))
61
61
 
62
62
  # Did we default to email?
63
- if email_adr is not None and uname is not None and uname.lower() == email_adr.lower():
63
+ if uname is not None and email_adr is not None and uname.lower() == email_adr.lower():
64
64
  # 1. Use provided regex matcher
65
65
  if provider_config.uid_regex:
66
66
  match = re.match(provider_config.uid_regex, uname)
@@ -93,6 +93,8 @@ def parse_profile(profile: dict[str, Any], provider_config: OAuthProvider) -> di
93
93
  # Compute access, roles and classification using auto_properties
94
94
  access = True
95
95
  roles = ["user"]
96
+ groups: list[str] = profile.get("groups", [])
97
+ assignments = []
96
98
  classification = CLASSIFICATION_ENGINE.UNRESTRICTED
97
99
  if provider_config.auto_properties:
98
100
  for auto_prop in provider_config.auto_properties:
@@ -129,18 +131,33 @@ def parse_profile(profile: dict[str, Any], provider_config: OAuthProvider) -> di
129
131
  classification = CLASSIFICATION_ENGINE.build_user_classification(
130
132
  classification, auto_prop.value
131
133
  )
134
+ logger.debug("Classification: %s", classification)
132
135
  break
133
136
 
134
- # Infer roles from groups
135
- if profile.get("groups") and provider_config.role_map:
137
+ # Append groups from matching patterns
138
+ elif auto_prop.type == "group":
139
+ if re.match(auto_prop.pattern, value):
140
+ groups.append(auto_prop.value)
141
+ break
142
+
143
+ # Append assignments from matching patterns
144
+ elif auto_prop.type == "assignment":
145
+ if re.match(auto_prop.pattern, value):
146
+ assignments.append(auto_prop.value)
147
+ break
148
+
149
+ # Infer roles from groups (legacy)
150
+ if groups and provider_config.role_map:
136
151
  for user_type in USER_TYPES:
137
152
  if (
138
153
  user_type in provider_config.role_map
139
- and provider_config.role_map[user_type] in profile.get("groups", [])
154
+ and provider_config.role_map[user_type] in groups
140
155
  and user_type not in roles
141
156
  ):
142
157
  roles.append(user_type)
143
158
 
159
+ # TODO: re-export assignments once they're actually used.
160
+ # This may need a refactor once the tags stuff is figured out
144
161
  return dict(
145
162
  access=access,
146
163
  type=roles,
@@ -150,7 +167,7 @@ def parse_profile(profile: dict[str, Any], provider_config: OAuthProvider) -> di
150
167
  email=email_adr,
151
168
  password="__NO_PASSWORD__", # noqa: S106
152
169
  avatar=profile.get("picture", provider_config.picture_url or alternate),
153
- groups=profile.get("groups", []),
170
+ groups=groups,
154
171
  )
155
172
 
156
173
 
@@ -166,9 +183,9 @@ def fetch_avatar( # noqa: C901
166
183
  # Generic picture url endpoint, i.e. MS Graph
167
184
  if url == provider_config.picture_url:
168
185
  headers = {}
169
-
170
186
  token: str | None = None
171
- if oauth_provider == "azure":
187
+
188
+ if oauth_provider in ["entraid", "azure"]:
172
189
  if not access_token:
173
190
  raise HowlerValueError("An azure access token is necessary to retrieve the profile picture") # noqa: TRY301
174
191
 
@@ -206,13 +223,13 @@ def fetch_avatar( # noqa: C901
206
223
  return None
207
224
 
208
225
 
209
- def fetch_groups(token: str):
210
- """Fetch a user's groups form an external endpoint"""
226
+ def fetch_groups(token: str): # noqa: C901
227
+ """Fetch a user's groups from an external endpoint"""
211
228
  oauth_provider = jwt_service.get_provider(token)
212
229
  oauth_provider_config = config.auth.oauth.providers[oauth_provider]
213
230
 
214
231
  if oauth_provider_config.groups_url:
215
- if oauth_provider == "azure":
232
+ if oauth_provider in ["entraid", "azure"]:
216
233
  try:
217
234
  token = azure_obo(token)
218
235
  except HowlerException:
@@ -232,13 +249,20 @@ def fetch_groups(token: str):
232
249
  result = result[part]
233
250
 
234
251
  detailed_group_data = []
252
+
235
253
  for group in result:
236
- detailed_group_data.append(
237
- {
238
- "id": group.get("id", None),
239
- "name": group.get("name", group.get("displayName", group.get("id", None))),
240
- }
241
- )
254
+ # Only process groups that are groups (not directory roles)
255
+ if group.get("@odata.type") == "#microsoft.graph.group":
256
+ group_id = group.get("id")
257
+ display_name = group.get("displayName")
258
+ if display_name: # Only add if display name exists
259
+ detailed_group_data.append(
260
+ {
261
+ "id": group_id,
262
+ "name": display_name,
263
+ }
264
+ )
265
+ logger.debug("Added group: %s (%s)", display_name, group_id)
242
266
 
243
267
  return sorted(detailed_group_data, key=lambda g: g.get("name", "").lower())
244
268
 
@@ -5,7 +5,7 @@ from howler.datastore.collection import ESCollection
5
5
  from howler.odm.models.user import User
6
6
 
7
7
  # List of indices where queries are protected with classification access control
8
- ACCESS_CONTROLLED_INDICES: dict[str, ESCollection] = {}
8
+ ACCESS_CONTROLLED_INDICES: set[str] = {"hit"}
9
9
 
10
10
  ADMIN_INDEX_MAP: dict[str, Callable[[], ESCollection]] = {}
11
11
 
@@ -176,7 +176,7 @@ class OAuthAutoProperty(BaseModel):
176
176
 
177
177
  field: str = Field(description="Field to apply `pattern` to")
178
178
  pattern: str = Field(description="Regex pattern for auto-prop assignment")
179
- type: Literal["access", "classification", "role"] = Field(
179
+ type: Literal["access", "classification", "role", "group", "assignment"] = Field(
180
180
  description="Type of property assignment on pattern match",
181
181
  )
182
182
  value: str = Field(description="Assigned property value")