cmdbsyncer 4.0.3__tar.gz → 4.1.0.dev2__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 (305) hide show
  1. {cmdbsyncer-4.0.3/cmdbsyncer.egg-info → cmdbsyncer-4.1.0.dev2}/PKG-INFO +2 -1
  2. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/__init__.py +94 -6
  3. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/api/__init__.py +32 -8
  4. cmdbsyncer-4.1.0.dev2/application/api/inventory.py +54 -0
  5. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/api/objects.py +169 -3
  6. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/api/rules.py +13 -2
  7. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/api/syncer.py +9 -4
  8. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/api/views.py +2 -0
  9. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/auth/views.py +129 -36
  10. cmdbsyncer-4.1.0.dev2/application/buildinfo.txt +1 -0
  11. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/changelog/v4.0.md +0 -15
  12. cmdbsyncer-4.1.0.dev2/application/changelog/v4.1.md +103 -0
  13. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/config.py +38 -0
  14. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/helpers/local_config_presets.py +5 -0
  15. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/models/account.py +5 -2
  16. cmdbsyncer-4.1.0.dev2/application/models/cron.py +204 -0
  17. cmdbsyncer-4.1.0.dev2/application/models/field_approval.py +55 -0
  18. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/models/forms.py +24 -12
  19. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/models/host.py +247 -12
  20. cmdbsyncer-4.1.0.dev2/application/models/host_inventory_tree.py +48 -0
  21. cmdbsyncer-4.1.0.dev2/application/models/saved_search.py +43 -0
  22. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/models/user.py +12 -5
  23. cmdbsyncer-4.1.0.dev2/application/modules/email.py +127 -0
  24. cmdbsyncer-4.1.0.dev2/application/modules/inventory/__init__.py +104 -0
  25. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/modules/plugin.py +41 -6
  26. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/ansible/__init__.py +185 -59
  27. cmdbsyncer-4.1.0.dev2/application/plugins/ansible/admin_views.py +93 -0
  28. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/ansible/inventory.py +5 -5
  29. cmdbsyncer-4.1.0.dev2/application/plugins/ansible/models.py +279 -0
  30. cmdbsyncer-4.1.0.dev2/application/plugins/ansible/playbook_rules.py +92 -0
  31. cmdbsyncer-4.1.0.dev2/application/plugins/ansible/runner.py +222 -0
  32. cmdbsyncer-4.1.0.dev2/application/plugins/ansible/views.py +556 -0
  33. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/__init__.py +67 -0
  34. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/admin_views.py +11 -0
  35. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/bi.py +2 -2
  36. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/cmk_rules.py +318 -1
  37. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/dcd.py +1 -1
  38. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/downtimes.py +1 -1
  39. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/groups.py +8 -7
  40. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/inits.py +35 -1
  41. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/inventorize.py +74 -4
  42. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/models.py +83 -1
  43. cmdbsyncer-4.1.0.dev2/application/plugins/checkmk/notification_rules.py +471 -0
  44. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/tests/test_bi.py +7 -7
  45. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/tests/test_cmk_rules.py +177 -0
  46. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/tests/test_groups.py +2 -2
  47. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/tests/test_inventorize.py +104 -3
  48. cmdbsyncer-4.1.0.dev2/application/plugins/checkmk/tests/test_notification_rules.py +436 -0
  49. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/views.py +508 -38
  50. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/cisco_dna/syncer.py +4 -1
  51. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/maintenance/__init__.py +72 -5
  52. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/rules/autorules.py +2 -2
  53. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/rules/rule_definitions.py +1 -0
  54. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/static/css/cmdbsyncer.css +25 -0
  55. cmdbsyncer-4.1.0.dev2/application/templates/_theme.html +27 -0
  56. cmdbsyncer-4.1.0.dev2/application/templates/admin/_host_tabs.html +91 -0
  57. cmdbsyncer-4.1.0.dev2/application/templates/admin/ansible_playbook_run.html +122 -0
  58. cmdbsyncer-4.1.0.dev2/application/templates/admin/ansible_project_detail.html +178 -0
  59. cmdbsyncer-4.1.0.dev2/application/templates/admin/ansible_rule_list.html +61 -0
  60. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/templates/admin/bulk_label_form.html +2 -1
  61. cmdbsyncer-4.1.0.dev2/application/templates/admin/checkmk_rule_mngmt_list.html +221 -0
  62. cmdbsyncer-4.1.0.dev2/application/templates/admin/data_quality.html +435 -0
  63. cmdbsyncer-4.1.0.dev2/application/templates/admin/host_details.html +11 -0
  64. cmdbsyncer-4.1.0.dev2/application/templates/admin/host_edit.html +13 -0
  65. cmdbsyncer-4.1.0.dev2/application/templates/admin/host_inventory_tree.html +356 -0
  66. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/templates/admin/host_list.html +78 -0
  67. cmdbsyncer-4.1.0.dev2/application/templates/admin/host_relations_graph.html +255 -0
  68. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/templates/admin/host_timeline.html +3 -6
  69. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/templates/admin/master.html +94 -9
  70. cmdbsyncer-4.1.0.dev2/application/templates/admin/model/inline_list_base.html +58 -0
  71. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/templates/admin/set_template_form.html +2 -1
  72. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/templates/base.html +1 -9
  73. cmdbsyncer-4.1.0.dev2/application/templates/change_password.html +19 -0
  74. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/templates/debug_host.html +149 -14
  75. cmdbsyncer-4.1.0.dev2/application/templates/email/resetpassword.html +83 -0
  76. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/templates/formular.html +1 -6
  77. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/templates/license_info.html +29 -0
  78. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/templates/login.html +2 -2
  79. cmdbsyncer-4.1.0.dev2/application/templates/request_password.html +240 -0
  80. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/templates/set_2fa.html +4 -2
  81. cmdbsyncer-4.1.0.dev2/application/templates/set_theme.html +38 -0
  82. cmdbsyncer-4.1.0.dev2/application/themes/dracula.css +234 -0
  83. cmdbsyncer-4.1.0.dev2/application/themes/gruvbox-dark.css +241 -0
  84. cmdbsyncer-4.1.0.dev2/application/themes/gruvbox-light.css +207 -0
  85. cmdbsyncer-4.1.0.dev2/application/themes/nord.css +241 -0
  86. cmdbsyncer-4.1.0.dev2/application/themes_registry.py +106 -0
  87. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/views/account.py +5 -28
  88. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/views/config.py +17 -2
  89. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/views/cron.py +95 -39
  90. cmdbsyncer-4.1.0.dev2/application/views/data_quality.py +322 -0
  91. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/views/default.py +10 -1
  92. cmdbsyncer-4.1.0.dev2/application/views/field_approval.py +168 -0
  93. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/views/host.py +870 -1124
  94. cmdbsyncer-4.1.0.dev2/application/views/host_filters.py +336 -0
  95. cmdbsyncer-4.1.0.dev2/application/views/host_renderers.py +838 -0
  96. cmdbsyncer-4.1.0.dev2/application/views/host_widgets.py +195 -0
  97. cmdbsyncer-4.1.0.dev2/application/views/license.py +156 -0
  98. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/views/notification_channel.py +2 -4
  99. cmdbsyncer-4.1.0.dev2/application/views/saved_search.py +130 -0
  100. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/views/user.py +13 -0
  101. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2/cmdbsyncer.egg-info}/PKG-INFO +2 -1
  102. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/cmdbsyncer.egg-info/SOURCES.txt +39 -2
  103. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/cmdbsyncer.egg-info/requires.txt +1 -0
  104. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/pyproject.toml +2 -1
  105. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/requirements-ansible.txt +1 -0
  106. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/tests/test_api.py +15 -3
  107. cmdbsyncer-4.1.0.dev2/tests/test_api_csrf_invariant.py +129 -0
  108. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/tests/test_plugin.py +75 -3
  109. cmdbsyncer-4.1.0.dev2/tests/test_webhook_token_hash.py +128 -0
  110. cmdbsyncer-4.0.3/application/buildinfo.txt +0 -1
  111. cmdbsyncer-4.0.3/application/models/cron.py +0 -129
  112. cmdbsyncer-4.0.3/application/modules/email.py +0 -52
  113. cmdbsyncer-4.0.3/application/plugins/ansible/admin_views.py +0 -41
  114. cmdbsyncer-4.0.3/application/plugins/ansible/models.py +0 -75
  115. cmdbsyncer-4.0.3/application/plugins/ansible/rest_api/ansible.py +0 -38
  116. cmdbsyncer-4.0.3/application/plugins/ansible/views.py +0 -20
  117. cmdbsyncer-4.0.3/application/templates/email/resetpassword.html +0 -9
  118. cmdbsyncer-4.0.3/application/views/license.py +0 -39
  119. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/LICENSE +0 -0
  120. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/README.md +0 -0
  121. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/_version.py +0 -0
  122. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/changelog/.pages +0 -0
  123. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/changelog/v3.10.md +0 -0
  124. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/changelog/v3.11.md +0 -0
  125. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/changelog/v3.12.md +0 -0
  126. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/changelog/v3.5.md +0 -0
  127. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/changelog/v3.7.md +0 -0
  128. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/changelog/v3.8.md +0 -0
  129. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/changelog/v3.9.md +0 -0
  130. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/cli.py +0 -0
  131. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/docu_links.py +0 -0
  132. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/enterprise.py +0 -0
  133. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/helpers/__init__.py +0 -0
  134. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/helpers/audit.py +0 -0
  135. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/helpers/cron.py +0 -0
  136. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/helpers/get_account.py +0 -0
  137. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/helpers/inventory.py +0 -0
  138. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/helpers/local_config_file.py +0 -0
  139. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/helpers/mongo_keys.py +0 -0
  140. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/helpers/notification_channels.py +0 -0
  141. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/helpers/notification_dispatch.py +0 -0
  142. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/helpers/plugins.py +0 -0
  143. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/helpers/sates.py +0 -0
  144. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/helpers/sql.py +0 -0
  145. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/helpers/syncer_jinja.py +0 -0
  146. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/helpers/tablib_formater.py +0 -0
  147. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/helpers/tests/__init__.py +0 -0
  148. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/helpers/tests/test_sql.py +0 -0
  149. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/mcp_server.py +0 -0
  150. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/models/__init__.py +0 -0
  151. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/models/config.py +0 -0
  152. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/models/notification_channel.py +0 -0
  153. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/models/notification_rule.py +0 -0
  154. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/models/notification_state.py +0 -0
  155. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/models/states.py +0 -0
  156. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/modules/__init__.py +0 -0
  157. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/modules/custom_attributes/models.py +0 -0
  158. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/modules/custom_attributes/rules.py +0 -0
  159. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/modules/custom_attributes/views.py +0 -0
  160. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/modules/debug.py +0 -0
  161. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/modules/log/log.py +0 -0
  162. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/modules/log/models.py +0 -0
  163. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/modules/log/views.py +0 -0
  164. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/modules/rule/__init__.py +0 -0
  165. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/modules/rule/filter.py +0 -0
  166. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/modules/rule/match.py +0 -0
  167. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/modules/rule/models.py +0 -0
  168. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/modules/rule/rewrite.py +0 -0
  169. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/modules/rule/rule.py +0 -0
  170. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/modules/rule/views.py +0 -0
  171. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/modules/statefile.py +0 -0
  172. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/notices/strict_certificate_verification.txt +0 -0
  173. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/__init__.py +0 -0
  174. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/ansible/rules.py +0 -0
  175. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/ansible/site_syncer.py +0 -0
  176. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/bmc_remedy/__init__.py +0 -0
  177. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/bmc_remedy/bmc_remedy.py +0 -0
  178. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/cmk2.py +0 -0
  179. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/helpers.py +0 -0
  180. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/import_v1.py +0 -0
  181. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/import_v2.py +0 -0
  182. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/passwords.py +0 -0
  183. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/poolfolder.py +0 -0
  184. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/rules.py +0 -0
  185. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/sites.py +0 -0
  186. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/syncer.py +0 -0
  187. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/tags.py +0 -0
  188. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/tests/__init__.py +0 -0
  189. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/tests/test_cmk2.py +0 -0
  190. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/tests/test_dcd.py +0 -0
  191. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/tests/test_downtimes.py +0 -0
  192. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/tests/test_helpers.py +0 -0
  193. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/tests/test_import_v1.py +0 -0
  194. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/tests/test_import_v2.py +0 -0
  195. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/tests/test_passwords.py +0 -0
  196. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/tests/test_poolfolder.py +0 -0
  197. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/tests/test_rules.py +0 -0
  198. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/tests/test_sites.py +0 -0
  199. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/tests/test_syncer.py +0 -0
  200. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/tests/test_tags.py +0 -0
  201. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/tests/test_users.py +0 -0
  202. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/checkmk/users.py +0 -0
  203. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/cisco_dna/__init__.py +0 -0
  204. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/cron.py +0 -0
  205. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/csv/__init__.py +0 -0
  206. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/csv/csv.py +0 -0
  207. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/file_requests.py +0 -0
  208. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/idoit/__init__.py +0 -0
  209. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/idoit/admin_views.py +0 -0
  210. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/idoit/models.py +0 -0
  211. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/idoit/rules.py +0 -0
  212. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/idoit/syncer.py +0 -0
  213. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/idoit/views.py +0 -0
  214. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/jdisc/__init__.py +0 -0
  215. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/jdisc/applications.py +0 -0
  216. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/jdisc/devices.py +0 -0
  217. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/jdisc/executables.py +0 -0
  218. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/jdisc/jdisc.py +0 -0
  219. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/jira/__init__.py +0 -0
  220. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/jira/jira.py +0 -0
  221. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/jira_cloud/__init__.py +0 -0
  222. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/jira_cloud/jira_cloud.py +0 -0
  223. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/json/__init__.py +0 -0
  224. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/json/json.py +0 -0
  225. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/ldap/__init__.py +0 -0
  226. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/ldap/ldap.py +0 -0
  227. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/mssql/__init__.py +0 -0
  228. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/mysql/__init__.py +0 -0
  229. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/mysql/mysql.py +0 -0
  230. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/netbox/__init__.py +0 -0
  231. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/netbox/admin_views.py +0 -0
  232. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/netbox/cluster.py +0 -0
  233. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/netbox/contacts.py +0 -0
  234. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/netbox/dataflow.py +0 -0
  235. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/netbox/devices.py +0 -0
  236. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/netbox/interfaces.py +0 -0
  237. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/netbox/ips.py +0 -0
  238. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/netbox/models.py +0 -0
  239. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/netbox/netbox.py +0 -0
  240. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/netbox/networks.py +0 -0
  241. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/netbox/prefixes.py +0 -0
  242. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/netbox/rules.py +0 -0
  243. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/netbox/views.py +0 -0
  244. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/netbox/virtualmachines.py +0 -0
  245. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/prtg/__init__.py +0 -0
  246. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/prtg/prtg.py +0 -0
  247. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/pyodbc/__init__.py +0 -0
  248. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/pyodbc/pyodbc.py +0 -0
  249. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/rest/__init__.py +0 -0
  250. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/rest/rest.py +0 -0
  251. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/rules/__init__.py +0 -0
  252. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/rules/admin_views.py +0 -0
  253. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/rules/models.py +0 -0
  254. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/rules/rule_import_export.py +0 -0
  255. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/rules/views.py +0 -0
  256. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/syncer_shell.py +0 -0
  257. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/vmware/__init__.py +0 -0
  258. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/vmware/admin_views.py +0 -0
  259. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/vmware/custom_attributes.py +0 -0
  260. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/vmware/models.py +0 -0
  261. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/vmware/rules.py +0 -0
  262. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/vmware/views.py +0 -0
  263. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/vmware/vmware.py +0 -0
  264. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/yml/__init__.py +0 -0
  265. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins/yml/yml.py +0 -0
  266. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/plugins_cli.py +0 -0
  267. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/static/js/main.js +0 -0
  268. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/static/logo_white.png +0 -0
  269. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/templates/_brand.html +0 -0
  270. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/templates/admin/account_pick_type.html +0 -0
  271. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/templates/admin/changelog_archive.html +0 -0
  272. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/templates/admin/checkmk_settings_list.html +0 -0
  273. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/templates/admin/copy_as_new_form.html +0 -0
  274. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/templates/admin/file/list.html +0 -0
  275. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/templates/admin/file/modals/form.html +0 -0
  276. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/templates/admin/index.html +0 -0
  277. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/templates/admin/local_config_editor.html +0 -0
  278. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/templates/admin/model/clone_confirm.html +0 -0
  279. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/templates/admin/object_list.html +0 -0
  280. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/templates/admin/set_cmk_version_form.html +0 -0
  281. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/templates/debug.html +0 -0
  282. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/templates/email/newuser.html +0 -0
  283. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/templates/email/notification.html +0 -0
  284. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/templates/logout_confirm.html +0 -0
  285. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/views/_form_fields.py +0 -0
  286. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/views/_form_sections.py +0 -0
  287. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/views/fileadmin.py +0 -0
  288. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/application/views/notification_rule.py +0 -0
  289. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/cmdbsyncer.egg-info/dependency_links.txt +0 -0
  290. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/cmdbsyncer.egg-info/entry_points.txt +0 -0
  291. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/cmdbsyncer.egg-info/top_level.txt +0 -0
  292. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/requirements-extras.txt +0 -0
  293. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/requirements.txt +0 -0
  294. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/setup.cfg +0 -0
  295. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/syncerapi/__init__.py +0 -0
  296. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/syncerapi/v1/__init__.py +0 -0
  297. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/syncerapi/v1/core/__init__.py +0 -0
  298. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/syncerapi/v1/inventory/__init__.py +0 -0
  299. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/syncerapi/v1/rest/__init__.py +0 -0
  300. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/tests/test_default_view.py +0 -0
  301. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/tests/test_mongo_keys.py +0 -0
  302. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/tests/test_plugin_ssti.py +0 -0
  303. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/tests/test_plugins_cli.py +0 -0
  304. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/tests/test_rule.py +0 -0
  305. {cmdbsyncer-4.0.3 → cmdbsyncer-4.1.0.dev2}/tests/test_rule_match.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cmdbsyncer
3
- Version: 4.0.3
3
+ Version: 4.1.0.dev2
4
4
  Summary: Rule-based modular synchronizer for Checkmk, Netbox, Ansible, I-DOIT, Jira CMDB and 15+ other CMDB/monitoring systems
5
5
  Author-email: Kuhn & Ruess GmbH <info@kuhn-ruess.de>
6
6
  Maintainer-email: Bastian Kuhn <mail@bastian-kuhn.de>
@@ -63,6 +63,7 @@ Requires-Dist: pykerberos==1.2.4; extra == "ansible"
63
63
  Requires-Dist: pywinrm==0.4.3; extra == "ansible"
64
64
  Requires-Dist: ntlm-auth==1.5.0; extra == "ansible"
65
65
  Requires-Dist: ansible==10.5.0; extra == "ansible"
66
+ Requires-Dist: cmdbsyncer-inventory==0.1.4; extra == "ansible"
66
67
  Dynamic: license-file
67
68
 
68
69
  # CMDBsyncer
@@ -202,6 +202,50 @@ if not CLI_MODE:
202
202
  """
203
203
  app.config['CHANGES'] = get_changes()
204
204
 
205
+ @app.context_processor
206
+ def _inject_pending_approvals():
207
+ """
208
+ Feed the navbar badge in master.html. Runs on every admin
209
+ request, so it must be cheap and fail-soft — the count is a
210
+ nicety, not a correctness requirement.
211
+ """
212
+ # pylint: disable=import-outside-toplevel
213
+ from flask_login import current_user as _user
214
+ if not app.config.get('CMDB_MODE'):
215
+ return {'pending_approval_count': 0}
216
+ try:
217
+ if not _user.is_authenticated:
218
+ return {'pending_approval_count': 0}
219
+ from application.views.field_approval import pending_count
220
+ count = pending_count()
221
+ except Exception: # pylint: disable=broad-exception-caught
222
+ return {'pending_approval_count': 0}
223
+ return {
224
+ 'pending_approval_count': count,
225
+ 'pending_approval_url': '/admin/fieldapproval/',
226
+ }
227
+
228
+ @app.context_processor
229
+ def _inject_user_theme():
230
+ """Expose the logged-in user's theme to ``_theme.html``.
231
+ Anonymous visitors and any error path fall back to the
232
+ default theme so the public templates (login, license)
233
+ render the same as before. Themes that no longer exist on
234
+ disk (e.g. an operator removed the CSS file) also fall
235
+ back, so a stale ``user.theme`` value never 404s."""
236
+ # pylint: disable=import-outside-toplevel
237
+ from flask_login import current_user as _user
238
+ from application.themes_registry import is_known
239
+ try:
240
+ if _user.is_authenticated:
241
+ slug = _user.theme or 'default'
242
+ if not is_known(slug):
243
+ slug = 'default'
244
+ return {'user_theme': slug}
245
+ except Exception: # pylint: disable=broad-exception-caught
246
+ pass
247
+ return {'user_theme': 'default'}
248
+
205
249
  @app.context_processor
206
250
  def _inject_https_warning():
207
251
  """Show a banner when HTTPS is required (default) but the
@@ -283,6 +327,12 @@ def _plugin_packages():
283
327
  return [p for p in (external_plugins_package, plugins_package) if p is not None]
284
328
 
285
329
 
330
+ # Cached list of dotted plugin module names. Populated by
331
+ # ``_import_all_plugins`` so the web-layer doesn't have to walk the
332
+ # plugin packages with ``pkgutil.iter_modules`` a second time at startup.
333
+ _DISCOVERED_PLUGIN_MODULES = []
334
+
335
+
286
336
  def _import_all_plugins():
287
337
  """Import every enabled plugin module so its side-effect registrations
288
338
  fire — Click CLI groups, sync hooks, cron entries.
@@ -292,8 +342,10 @@ def _import_all_plugins():
292
342
  here; they are web-only and handled by
293
343
  ``_register_all_plugin_admin_views``.
294
344
  """
345
+ _DISCOVERED_PLUGIN_MODULES.clear()
295
346
  for package in _plugin_packages():
296
347
  for module_name in _iter_enabled_plugin_modules(package):
348
+ _DISCOVERED_PLUGIN_MODULES.append(module_name)
297
349
  try:
298
350
  importlib.import_module(module_name)
299
351
  except Exception: # pylint: disable=broad-exception-caught
@@ -303,11 +355,7 @@ def _import_all_plugins():
303
355
 
304
356
 
305
357
  def _register_all_plugin_admin_views(admin_):
306
- plugin_modules = []
307
- for package in _plugin_packages():
308
- plugin_modules.extend(_iter_enabled_plugin_modules(package))
309
-
310
- for module_name in plugin_modules:
358
+ for module_name in _DISCOVERED_PLUGIN_MODULES:
311
359
  admin_module_name = f"{module_name}.admin_views"
312
360
  try:
313
361
  admin_module = importlib.import_module(admin_module_name)
@@ -343,8 +391,19 @@ def _register_web_layer(): # pylint: disable=too-many-locals,too-many-statement
343
391
  from application.auth.views import AUTH
344
392
  app.register_blueprint(AUTH)
345
393
 
394
+ from application.themes_registry import init_themes, themes_blueprint
395
+ init_themes(app)
396
+ app.register_blueprint(themes_blueprint)
397
+
346
398
  from application.api.views import API_BP as api
347
399
  app.register_blueprint(api, url_prefix="/api/v1")
400
+ # Intentional: every /api/v1 route authenticates per request via
401
+ # @require_token (Basic Auth) or @require_api_role, so CSRF tokens
402
+ # would only break external clients (CLI, MCP, Ansible inventory
403
+ # pulls, Grafana scrapes — none of which fetch a CSRF cookie). The
404
+ # invariant that no /api/v1/* route falls back to session auth is
405
+ # enforced by tests/test_api_csrf_invariant.py — DO NOT remove this
406
+ # exempt during a code review without first reading that test.
348
407
  csrf.exempt(api)
349
408
 
350
409
  # Give the enterprise package a chance to register its own auth-related
@@ -383,9 +442,14 @@ def _register_web_layer(): # pylint: disable=too-many-locals,too-many-statement
383
442
 
384
443
  # .-- Host
385
444
  from application.models.host import Host
386
- from application.views.host import HostModelView, ObjectModelView, TemplateModelView
445
+ from application.views.host import (
446
+ HostArchiveView, HostModelView, ObjectModelView, TemplateModelView,
447
+ )
387
448
  admin.add_view(HostModelView(Host, name="Hosts",
388
449
  menu_icon_type='fa', menu_icon_value='fa-server'))
450
+
451
+
452
+
389
453
  admin.add_category(name="Objects", icon_type='fa', icon_value='fa-folder-open')
390
454
  admin.add_view(ObjectModelView(Host, name="All Objects", endpoint="Objects",
391
455
  category="Objects",
@@ -393,6 +457,14 @@ def _register_web_layer(): # pylint: disable=too-many-locals,too-many-statement
393
457
  admin.add_view(TemplateModelView(Host, name="Templates", endpoint="Objects Templates",
394
458
  category="Objects",
395
459
  menu_icon_type='fa', menu_icon_value='fa-files-o'))
460
+ admin.add_view(HostArchiveView(Host, name="Archive", endpoint="archive",
461
+ category="Objects",
462
+ menu_icon_type='fa', menu_icon_value='fa-archive'))
463
+ from application.views.data_quality import DataQualityView
464
+ admin.add_view(DataQualityView(name="Data Quality", endpoint="data_quality",
465
+ category="Objects",
466
+ menu_icon_type='fa',
467
+ menu_icon_value='fa-stethoscope'))
396
468
  #.
397
469
  # .-- Global
398
470
  from application.modules.custom_attributes.models import CustomAttributeRule
@@ -438,6 +510,12 @@ def _register_web_layer(): # pylint: disable=too-many-locals,too-many-statement
438
510
  admin.add_sub_category(name="Compliance", parent_name="Settings")
439
511
  admin.add_sub_category(name="Notifications", parent_name="Settings")
440
512
  admin.add_sub_category(name="Backups", parent_name="Settings")
513
+ from application.models.field_approval import FieldApproval
514
+ from application.views.field_approval import FieldApprovalView
515
+ admin.add_view(FieldApprovalView(FieldApproval, name="Approvals",
516
+ category="Settings",
517
+ menu_icon_type='fa',
518
+ menu_icon_value='fa-hourglass-half'))
441
519
 
442
520
  from application.modules.log.models import LogEntry
443
521
  from application.modules.log.views import LogView
@@ -459,6 +537,13 @@ def _register_web_layer(): # pylint: disable=too-many-locals,too-many-statement
459
537
  endpoint='config.local_config_editor',
460
538
  icon_type='fa', icon_value='fa-file-code-o'))
461
539
 
540
+ from application.models.saved_search import SavedSearch
541
+ from application.views.saved_search import SavedSearchView
542
+ admin.add_view(SavedSearchView(SavedSearch, name="Saved Searches",
543
+ category="Settings",
544
+ menu_icon_type='fa',
545
+ menu_icon_value='fa-bookmark'))
546
+
462
547
  from application.views.license import LicenseView
463
548
  admin.add_view(LicenseView(name="License", endpoint="license", category="Settings",
464
549
  menu_icon_type='fa', menu_icon_value='fa-id-card'))
@@ -493,6 +578,9 @@ def _register_web_layer(): # pylint: disable=too-many-locals,too-many-statement
493
578
  admin.add_link(MenuLink(name='Set 2FA Code', category='Account',
494
579
  url=f"{app.config['BASE_PREFIX']}set-2fa",
495
580
  icon_type='fa', icon_value='fa-shield'))
581
+ admin.add_link(MenuLink(name='Theme', category='Account',
582
+ url=f"{app.config['BASE_PREFIX']}set-theme",
583
+ icon_type='fa', icon_value='fa-paint-brush'))
496
584
  admin.add_link(MenuLink(name='Logout', category='Account',
497
585
  url=f"{app.config['BASE_PREFIX']}logout",
498
586
  icon_type='fa', icon_value='fa-sign-out'))
@@ -5,6 +5,7 @@ API
5
5
  from functools import wraps
6
6
  from flask import abort, request, current_app
7
7
  from mongoengine.errors import DoesNotExist, MultipleObjectsReturned
8
+ from application.helpers.audit import audit
8
9
  from application.models.account import Account
9
10
  from application.models.user import User
10
11
  from application import log
@@ -73,15 +74,38 @@ def _authenticate_user():
73
74
  except DoesNotExist:
74
75
  _abort_unauthorized("Invalid credentials")
75
76
  except MultipleObjectsReturned:
76
- user_result = next(
77
- (candidate for candidate in User.objects(
78
- disabled__ne=True,
79
- __raw__={'$or': [{'name': username}, {'email': username}]}
80
- ) if candidate.check_password(user_password)),
81
- None,
82
- )
83
- if user_result is None:
77
+ # Several users match the same login string. Historically this
78
+ # happens when the `name` field (added later, sparse) accidentally
79
+ # matches another account's `email`. We can't drop the branch —
80
+ # purging or unique-indexing the existing duplicates breaks logins
81
+ # for users whose `name` was empty when the field was introduced.
82
+ # So: walk every candidate (constant-time, no early exit) and
83
+ # surface the collision as an audit event so it can be cleaned up.
84
+ candidates = list(User.objects(
85
+ disabled__ne=True,
86
+ __raw__={'$or': [{'name': username}, {'email': username}]}
87
+ ))
88
+ # Constant-time match: check every candidate so a wrong password
89
+ # against the first colliding user takes the same time as the
90
+ # last, which keeps timing from leaking *which* duplicate exists.
91
+ match = None
92
+ for candidate in candidates:
93
+ if candidate.check_password(user_password) and match is None:
94
+ match = candidate
95
+ audit('user.login.collision',
96
+ outcome='success' if match else 'failure',
97
+ actor_type='user',
98
+ actor_id=str(match.id) if match else None,
99
+ actor_name=username,
100
+ metadata={
101
+ 'reason': 'multiple users matched login string',
102
+ 'candidate_count': len(candidates),
103
+ 'candidate_ids': [str(c.id) for c in candidates],
104
+ 'matched_id': str(match.id) if match else None,
105
+ })
106
+ if match is None:
84
107
  _abort_unauthorized("Invalid credentials")
108
+ return match, username
85
109
  if not user_result.check_password(user_password):
86
110
  _abort_unauthorized("Invalid credentials")
87
111
  return user_result, username
@@ -0,0 +1,54 @@
1
+ """
2
+ Ansible namespace — inventory provider HTTP endpoints.
3
+
4
+ The cross-module provider registry (`application.modules.inventory`)
5
+ holds named providers. This namespace exposes them in the Ansible JSON
6
+ shape under `/api/v1/ansible/inventory/<provider>` — same convention
7
+ as the CLI (`cmdbsyncer ansible inventory <provider>`), so URL and
8
+ command path read in the same order.
9
+
10
+ Both the CLI and this HTTP endpoint share `render_ansible_inventory`,
11
+ so data and format stay in lockstep regardless of how a consumer
12
+ reaches them.
13
+ """
14
+ from flask import request
15
+ from flask_restx import Namespace, Resource
16
+
17
+ from application.api import require_token
18
+ from application.modules.inventory import (
19
+ list_inventory_providers,
20
+ render_ansible_inventory,
21
+ )
22
+
23
+ API = Namespace('ansible', description='Ansible-side endpoints (inventory provider)')
24
+
25
+
26
+ @API.route('/inventory')
27
+ class AnsibleProviderIndex(Resource):
28
+ """List the providers the Ansible-format adapter can serve."""
29
+
30
+ @require_token
31
+ def get(self):
32
+ """Names of all registered inventory providers."""
33
+ return {'providers': list_inventory_providers()}
34
+
35
+
36
+ @API.route('/inventory/<provider>')
37
+ class AnsibleProviderInventory(Resource):
38
+ """Full inventory for `provider`, in Ansible JSON shape.
39
+
40
+ Pass `?host=NAME` to get a single host's vars dict instead of the
41
+ full catalog — same contract Ansible expects from a dynamic
42
+ inventory script.
43
+ """
44
+
45
+ @require_token
46
+ def get(self, provider):
47
+ """Render the provider in Ansible-format JSON."""
48
+ host = request.args.get('host')
49
+ result = render_ansible_inventory(provider, host=host)
50
+ if result is None:
51
+ return {'message': f'Unknown provider: {provider}'}, 404
52
+ if result is False:
53
+ return {'message': f'Host not found: {host}'}, 404
54
+ return result
@@ -11,8 +11,11 @@ from mongoengine.errors import DoesNotExist
11
11
 
12
12
  from flask import request, abort
13
13
  from flask_restx import Namespace, Resource, reqparse, fields
14
+ from application import app
14
15
  from application.api import require_token
15
- from application.models.host import Host
16
+ from application.models.host import (
17
+ Host, HostError, RELATION_TYPES, RELATION_INVERSE_LABEL,
18
+ )
16
19
  # @TODO pre_deletion method for Host so no import needed
17
20
  from application.plugins.checkmk.models import CheckmkFolderPool
18
21
 
@@ -164,6 +167,41 @@ LIST_RESPONSE = API.model('list_response', {
164
167
  '_links': fields.Nested(LIST_LINKS),
165
168
  })
166
169
 
170
+ _RELATION_TYPE_VALUES = [t for t, _ in RELATION_TYPES]
171
+
172
+ RELATION_INPUT = API.model('relation_input', {
173
+ 'type': fields.String(required=True, enum=_RELATION_TYPE_VALUES,
174
+ description='Edge type. One of: '
175
+ + ', '.join(_RELATION_TYPE_VALUES)),
176
+ 'target': fields.String(required=True,
177
+ description='Hostname of the target object.'),
178
+ 'source': fields.String(required=False, default='manual',
179
+ description='Free-form provenance tag '
180
+ '(default "manual"). Stored on the '
181
+ 'edge so importers can later prune '
182
+ 'their own edges without touching '
183
+ 'manual ones.'),
184
+ })
185
+
186
+ RELATION_ITEM = API.model('relation_item', {
187
+ 'type': fields.String(description='Edge type code'),
188
+ 'type_label': fields.String(description='Human label for the type'),
189
+ 'target': fields.String(description='Target hostname'),
190
+ 'source': fields.String(description='Provenance tag (manual / import / …)'),
191
+ })
192
+
193
+ RELATIONS_RESPONSE = API.model('relations_response', {
194
+ 'hostname': fields.String,
195
+ 'outgoing': fields.List(fields.Nested(RELATION_ITEM)),
196
+ 'inbound': fields.List(fields.Nested(RELATION_ITEM)),
197
+ })
198
+
199
+
200
+ def _require_cmdb_mode():
201
+ """Abort with 404 unless CMDB_MODE is enabled."""
202
+ if not app.config.get('CMDB_MODE'):
203
+ abort(404, 'CMDB_MODE is disabled on this install.')
204
+
167
205
 
168
206
  # ---------------------------------------------------------------------------
169
207
  # Endpoints
@@ -393,9 +431,13 @@ parser.add_argument('start', type=int, help='Pagination start')
393
431
  parser.add_argument('limit', type=int, help='Pagination limit')
394
432
 
395
433
 
434
+ MAX_PAGE_LIMIT = 10000
435
+
436
+
396
437
  @API.route('/all')
397
438
  @API.param('start', 'Zero-based page offset. Default ``1``.', type='integer')
398
- @API.param('limit', 'Page size, max items returned per call. Default ``100``.',
439
+ @API.param('limit', 'Page size, max items returned per call. Default ``100``,'
440
+ f' max ``{MAX_PAGE_LIMIT}``.',
399
441
  type='integer')
400
442
  class HostDetailListApi(Resource):
401
443
  """Paginated listing of every host."""
@@ -403,7 +445,8 @@ class HostDetailListApi(Resource):
403
445
  @API.doc(security=['x-login-user', 'basicAuth'])
404
446
  @API.response(200, 'A page of host objects plus pagination links.',
405
447
  LIST_RESPONSE)
406
- @API.response(400, 'Pagination params not numeric or negative.', ERROR)
448
+ @API.response(400, 'Pagination params not numeric, negative, or limit '
449
+ f'above {MAX_PAGE_LIMIT}.', ERROR)
407
450
  @API.response(401, 'Authentication failed', ERROR)
408
451
  @require_token
409
452
  def get(self):
@@ -420,6 +463,8 @@ class HostDetailListApi(Resource):
420
463
  abort(400, "start and limit must be integers")
421
464
  if start < 0 or limit < 0:
422
465
  abort(400, "start and limit must be non-negative")
466
+ if limit > MAX_PAGE_LIMIT:
467
+ abort(400, f"limit must be <= {MAX_PAGE_LIMIT}")
423
468
  end = start+limit
424
469
 
425
470
  db_objecs = Host.objects(is_object__ne=True)
@@ -441,3 +486,124 @@ class HostDetailListApi(Resource):
441
486
  'size': total,
442
487
  '_links': links,
443
488
  }
489
+
490
+
491
+ @API.route('/<hostname>/relations')
492
+ @API.param('hostname', 'Existing host name. CMDB_MODE only.')
493
+ class HostRelationsApi(Resource):
494
+ """
495
+ Read, add or remove typed Host relations.
496
+
497
+ Only available when ``CMDB_MODE`` is True. Outgoing edges live on
498
+ the source host; inbound edges are computed via a Mongo query and
499
+ are read-only on this endpoint (mutate them on the source host).
500
+ """
501
+
502
+ @staticmethod
503
+ def _serialize_outgoing(host):
504
+ type_label = dict(RELATION_TYPES)
505
+ out = []
506
+ for rel in (host.relations or []):
507
+ target = rel.target_host
508
+ if not target:
509
+ continue
510
+ out.append({
511
+ 'type': rel.type,
512
+ 'type_label': type_label.get(rel.type, rel.type),
513
+ 'target': target.hostname,
514
+ 'source': rel.source or '',
515
+ })
516
+ return out
517
+
518
+ @staticmethod
519
+ def _serialize_inbound(host):
520
+ if not host.pk:
521
+ return []
522
+ out = []
523
+ sources = Host.objects(__raw__={'relations.target_host': host.pk}).only(
524
+ 'hostname', 'relations'
525
+ )
526
+ for src in sources:
527
+ for rel in (src.relations or []):
528
+ if rel.target_host and rel.target_host.pk == host.pk:
529
+ out.append({
530
+ 'type': rel.type,
531
+ 'type_label': RELATION_INVERSE_LABEL.get(rel.type,
532
+ rel.type),
533
+ 'target': src.hostname,
534
+ 'source': rel.source or '',
535
+ })
536
+ return out
537
+
538
+ @API.doc(security=['x-login-user', 'basicAuth'])
539
+ @API.response(200, 'Outgoing and inbound edges for this host.',
540
+ RELATIONS_RESPONSE)
541
+ @API.response(401, 'Authentication failed', ERROR)
542
+ @API.response(404, 'Host not found or CMDB_MODE disabled.', ERROR)
543
+ @require_token
544
+ def get(self, hostname):
545
+ """List outgoing + inbound relations for *hostname*."""
546
+ _require_cmdb_mode()
547
+ try:
548
+ host = Host.objects.get(hostname=hostname)
549
+ except DoesNotExist:
550
+ return {'error': 'Host not found'}, 404
551
+ return {
552
+ 'hostname': hostname,
553
+ 'outgoing': self._serialize_outgoing(host),
554
+ 'inbound': self._serialize_inbound(host),
555
+ }
556
+
557
+ @API.doc(security=['x-login-user', 'basicAuth'])
558
+ @API.expect(RELATION_INPUT, validate=True)
559
+ @API.response(200, 'Relation added (or no-op if it already existed).',
560
+ STATUS)
561
+ @API.response(400, 'Unknown relation type or self-edge.', ERROR)
562
+ @API.response(401, 'Authentication failed', ERROR)
563
+ @API.response(404, 'Source or target host not found, or CMDB_MODE off.',
564
+ ERROR)
565
+ @require_token
566
+ def post(self, hostname):
567
+ """Add a relation from *hostname* to ``target``."""
568
+ _require_cmdb_mode()
569
+ body = request.json or {}
570
+ target_name = body.get('target')
571
+ rtype = body.get('type')
572
+ rsource = body.get('source') or 'manual'
573
+ try:
574
+ host = Host.objects.get(hostname=hostname)
575
+ target = Host.objects.get(hostname=target_name)
576
+ except DoesNotExist:
577
+ return {'error': 'Host or target not found'}, 404
578
+ try:
579
+ added = host.add_relation(target, rtype, source=rsource)
580
+ except HostError as exc:
581
+ abort(400, str(exc))
582
+ if added:
583
+ host.save()
584
+ return {'status': 'added'}, 200
585
+ return {'status': 'unchanged'}, 200
586
+
587
+ @API.doc(security=['x-login-user', 'basicAuth'])
588
+ @API.expect(RELATION_INPUT, validate=True)
589
+ @API.response(200, 'Relation removed (or no-op if it never existed).',
590
+ STATUS)
591
+ @API.response(401, 'Authentication failed', ERROR)
592
+ @API.response(404, 'Source or target host not found, or CMDB_MODE off.',
593
+ ERROR)
594
+ @require_token
595
+ def delete(self, hostname):
596
+ """Remove the relation ``(type, target)`` from *hostname*."""
597
+ _require_cmdb_mode()
598
+ body = request.json or {}
599
+ target_name = body.get('target')
600
+ rtype = body.get('type')
601
+ try:
602
+ host = Host.objects.get(hostname=hostname)
603
+ target = Host.objects.get(hostname=target_name)
604
+ except DoesNotExist:
605
+ return {'error': 'Host or target not found'}, 404
606
+ if host.remove_relation(target, rtype):
607
+ host.save()
608
+ return {'status': 'removed'}, 200
609
+ return {'status': 'unchanged'}, 200
@@ -14,6 +14,7 @@ import json
14
14
  from flask import request
15
15
  from flask_restx import Namespace, Resource, fields
16
16
 
17
+ from application import log
17
18
  from application.api import require_token
18
19
  from application.plugins.rules.rule_definitions import rules as enabled_rules
19
20
  from application.plugins.rules.rule_import_export import (
@@ -105,11 +106,21 @@ AUTORULES_RESPONSE = API.model('autorules_response', {
105
106
 
106
107
 
107
108
  def _decode_rule_lines(rules):
108
- """Yield JSON dicts from ``to_json()`` strings."""
109
+ """Yield JSON dicts from ``to_json()`` strings.
110
+
111
+ Skipped lines are surfaced in the syncer Log so a corrupted rule
112
+ document doesn't silently disappear from /rules/<type> exports.
113
+ """
109
114
  for raw in rules:
110
115
  try:
111
116
  yield json.loads(raw)
112
- except (ValueError, TypeError):
117
+ except (ValueError, TypeError) as exp:
118
+ preview = (raw[:200] + '…') if isinstance(raw, str) and len(raw) > 200 else raw
119
+ log.log(
120
+ f"Skipping rule with invalid JSON: {exp}",
121
+ source='api.rules',
122
+ details=[('error', str(exp)), ('preview', str(preview))],
123
+ )
113
124
  continue
114
125
 
115
126
 
@@ -8,7 +8,6 @@ namespace. The ``/cron/trigger/<group>`` route additionally accepts an
8
8
  ``X-Webhook-Token`` header tied to the CronGroup, so external systems
9
9
  can fire a sync without carrying a user credential.
10
10
  """
11
- import hmac
12
11
  from datetime import datetime, timedelta
13
12
  from mongoengine.errors import DoesNotExist
14
13
  from flask import request
@@ -302,7 +301,13 @@ class SyncerCronTriggerApi(Resource):
302
301
 
303
302
  token = request.headers.get('X-Webhook-Token')
304
303
  if auth_method is None and token:
305
- if not group.webhook_enabled or not group.webhook_token:
304
+ # Hash-on-read: legacy rows still carrying plaintext get
305
+ # upgraded the first time they authenticate after the
306
+ # rollout, so operators don't have to run a separate
307
+ # migration step.
308
+ if group.migrate_legacy_webhook_token():
309
+ group.save()
310
+ if not group.webhook_enabled or not group.webhook_token_hash:
306
311
  log.log("Webhook trigger rejected", source="API",
307
312
  details=[('group', group_name),
308
313
  ('reason', 'webhook disabled or no token set'),
@@ -313,8 +318,8 @@ class SyncerCronTriggerApi(Resource):
313
318
  target_name=group_name,
314
319
  metadata={'reason': 'webhook disabled or no token set'})
315
320
  return {'error': "Webhook not enabled for this group"}, 403
316
- # hmac.compare_digest prevents timing-based token discovery.
317
- if not hmac.compare_digest(token, group.webhook_token):
321
+ # verify_webhook_token uses hmac.compare_digest internally.
322
+ if not group.verify_webhook_token(token):
318
323
  log.log("Webhook trigger rejected", source="API",
319
324
  details=[('group', group_name),
320
325
  ('reason', 'token mismatch'),
@@ -8,6 +8,7 @@ from application import app, limiter
8
8
  from application.api.syncer import API as syncer
9
9
  from application.api.objects import API as objects
10
10
  from application.api.rules import API as rules
11
+ from application.api.inventory import API as ansible
11
12
 
12
13
  API_BP = Blueprint('api', __name__)
13
14
 
@@ -52,3 +53,4 @@ API = Api(API_BP, authorizations=AUTHORIZATIONS,
52
53
  API.add_namespace(syncer, path='/syncer')
53
54
  API.add_namespace(objects, path='/objects')
54
55
  API.add_namespace(rules, path='/rules')
56
+ API.add_namespace(ansible, path='/ansible')