django-cfg 1.4.87__py3-none-any.whl → 1.4.89__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of django-cfg might be problematic. Click here for more details.

Files changed (177) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/centrifugo/views/__init__.py +0 -2
  3. django_cfg/apps/dashboard/permissions.py +48 -0
  4. django_cfg/apps/dashboard/serializers/__init__.py +8 -1
  5. django_cfg/apps/dashboard/serializers/commands.py +29 -0
  6. django_cfg/apps/dashboard/services/__init__.py +2 -0
  7. django_cfg/{modules/django_unfold/callbacks/base.py → apps/dashboard/services/commands_security.py} +28 -90
  8. django_cfg/apps/dashboard/services/commands_service.py +208 -9
  9. django_cfg/apps/dashboard/services/overview_service.py +205 -0
  10. django_cfg/apps/dashboard/views/commands_views.py +92 -4
  11. django_cfg/apps/frontend/test_routing.py +134 -0
  12. django_cfg/apps/frontend/views.py +73 -28
  13. django_cfg/apps/urls.py +0 -1
  14. django_cfg/core/builders/apps_builder.py +0 -58
  15. django_cfg/modules/django_unfold/__init__.py +5 -24
  16. django_cfg/modules/django_unfold/models/__init__.py +0 -23
  17. django_cfg/modules/django_unfold/models/config.py +11 -65
  18. django_cfg/modules/django_unfold/{dashboard.py → navigation.py} +21 -152
  19. django_cfg/modules/django_unfold/tailwind.py +2 -4
  20. django_cfg/pyproject.toml +1 -1
  21. django_cfg/registry/third_party.py +0 -9
  22. django_cfg/routing/callbacks.py +1 -43
  23. django_cfg/static/frontend/admin/404/index.html +1 -1
  24. django_cfg/static/frontend/admin/404.html +1 -1
  25. django_cfg/static/frontend/admin/500/index.html +1 -1
  26. django_cfg/static/frontend/admin/_next/static/{ZJZBgOL9mO1koHrgaaLEV → 0sN9ktsgXH48ygtGSrhfu}/_buildManifest.js +1 -1
  27. django_cfg/static/frontend/admin/_next/static/chunks/{19430.fe7bff7372f8a256.js → 19430.c4c95603c23c17fe.js} +1 -1
  28. django_cfg/static/frontend/admin/_next/static/chunks/50314-9443faa6df24aebf.js +1 -0
  29. django_cfg/static/frontend/admin/_next/static/chunks/94141-bc6d47f419b26b21.js +1 -0
  30. django_cfg/static/frontend/admin/_next/static/chunks/pages/{_app-c336f254967dd101.js → _app-c7dcd3aa616fab68.js} +6 -6
  31. django_cfg/static/frontend/admin/_next/static/chunks/pages/legal/{cookies-b39c7f22c066e2c6.js → cookies-97d279800f12aab4.js} +1 -1
  32. django_cfg/static/frontend/admin/_next/static/chunks/pages/legal/{privacy-5aedad0cf3a4f80f.js → privacy-1d5e6cd94689247e.js} +1 -1
  33. django_cfg/static/frontend/admin/_next/static/chunks/pages/legal/{security-dbd854d0d5d483e2.js → security-55e49700e7a01f5a.js} +1 -1
  34. django_cfg/static/frontend/admin/_next/static/chunks/pages/legal/{terms-f3e1d2b9e5edf12f.js → terms-14c02bb2d3198352.js} +1 -1
  35. django_cfg/static/frontend/admin/_next/static/chunks/pages/private/{centrifugo-22532c65971225eb.js → centrifugo-f9ecbc3ae0052a03.js} +1 -1
  36. django_cfg/static/frontend/admin/_next/static/chunks/pages/private-d4ccbe1265cbd853.js +1 -0
  37. django_cfg/static/frontend/admin/_next/static/chunks/{webpack-da114020a6b940f5.js → webpack-5a92f81363b62aa7.js} +1 -1
  38. django_cfg/static/frontend/admin/_next/static/css/3063068f0d5a8a00.css +3 -0
  39. django_cfg/static/frontend/admin/_next/static/media/19cfc7226ec3afaa-s.woff2 +0 -0
  40. django_cfg/static/frontend/admin/_next/static/media/21350d82a1f187e9-s.p.woff2 +0 -0
  41. django_cfg/static/frontend/admin/_next/static/media/8e9860b6e62d6359-s.woff2 +0 -0
  42. django_cfg/static/frontend/admin/_next/static/media/ba9851c3c22cd980-s.woff2 +0 -0
  43. django_cfg/static/frontend/admin/_next/static/media/c5fe6dc8356a8c31-s.woff2 +0 -0
  44. django_cfg/static/frontend/admin/_next/static/media/df0a9ae256c0569c-s.woff2 +0 -0
  45. django_cfg/static/frontend/admin/_next/static/media/e4af272ccee01ff0-s.p.woff2 +0 -0
  46. django_cfg/static/frontend/admin/auth/index.html +1 -1
  47. django_cfg/static/frontend/admin/index.html +1 -1
  48. django_cfg/static/frontend/admin/legal/cookies/index.html +1 -1
  49. django_cfg/static/frontend/admin/legal/privacy/index.html +1 -1
  50. django_cfg/static/frontend/admin/legal/security/index.html +1 -1
  51. django_cfg/static/frontend/admin/legal/terms/index.html +1 -1
  52. django_cfg/static/frontend/admin/private/centrifugo/index.html +1 -1
  53. django_cfg/static/frontend/admin/private/index.html +1 -1
  54. django_cfg/static/frontend/admin/private/profile/index.html +1 -1
  55. django_cfg/static/frontend/admin/private/ui/index.html +2 -2
  56. django_cfg/templates/admin/index.html +1 -1
  57. {django_cfg-1.4.87.dist-info → django_cfg-1.4.89.dist-info}/METADATA +1 -1
  58. {django_cfg-1.4.87.dist-info → django_cfg-1.4.89.dist-info}/RECORD +62 -163
  59. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/css/dashboard.css +0 -260
  60. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/live_channels.mjs +0 -313
  61. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/live_testing.mjs +0 -803
  62. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/main.mjs +0 -341
  63. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/overview.mjs +0 -432
  64. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/testing.mjs +0 -33
  65. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/websocket.mjs +0 -210
  66. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/channels_content.html +0 -46
  67. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/live_channels_content.html +0 -123
  68. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/overview_content.html +0 -45
  69. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/publishes_content.html +0 -84
  70. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/stat_cards.html +0 -53
  71. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/system_status.html +0 -91
  72. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/tab_navigation.html +0 -29
  73. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/testing_tools.html +0 -415
  74. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/layout/base.html +0 -61
  75. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/pages/dashboard.html +0 -58
  76. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/tags/connection_script.html +0 -48
  77. django_cfg/apps/centrifugo/templatetags/__init__.py +0 -1
  78. django_cfg/apps/centrifugo/templatetags/centrifugo_tags.py +0 -81
  79. django_cfg/apps/centrifugo/urls_admin.py +0 -20
  80. django_cfg/apps/centrifugo/views/dashboard.py +0 -28
  81. django_cfg/modules/django_dashboard/__init__.py +0 -23
  82. django_cfg/modules/django_dashboard/components.py +0 -312
  83. django_cfg/modules/django_dashboard/debug.py +0 -174
  84. django_cfg/modules/django_dashboard/management/__init__.py +0 -0
  85. django_cfg/modules/django_dashboard/management/commands/__init__.py +0 -0
  86. django_cfg/modules/django_dashboard/management/commands/debug_dashboard.py +0 -109
  87. django_cfg/modules/django_dashboard/sections/__init__.py +0 -1
  88. django_cfg/modules/django_dashboard/sections/base.py +0 -129
  89. django_cfg/modules/django_dashboard/sections/commands.py +0 -33
  90. django_cfg/modules/django_dashboard/sections/documentation.py +0 -393
  91. django_cfg/modules/django_dashboard/sections/overview.py +0 -398
  92. django_cfg/modules/django_dashboard/sections/stats.py +0 -48
  93. django_cfg/modules/django_dashboard/sections/system.py +0 -74
  94. django_cfg/modules/django_dashboard/sections/widgets.py +0 -222
  95. django_cfg/modules/django_unfold/callbacks/__init__.py +0 -9
  96. django_cfg/modules/django_unfold/callbacks/actions.py +0 -51
  97. django_cfg/modules/django_unfold/callbacks/apizones.py +0 -122
  98. django_cfg/modules/django_unfold/callbacks/charts.py +0 -223
  99. django_cfg/modules/django_unfold/callbacks/commands.py +0 -40
  100. django_cfg/modules/django_unfold/callbacks/main.py +0 -322
  101. django_cfg/modules/django_unfold/callbacks/statistics.py +0 -240
  102. django_cfg/modules/django_unfold/callbacks/system.py +0 -180
  103. django_cfg/modules/django_unfold/callbacks/users.py +0 -65
  104. django_cfg/modules/django_unfold/models/dashboard.py +0 -207
  105. django_cfg/modules/django_unfold/models/tabs.py +0 -26
  106. django_cfg/modules/django_unfold/models.py +0 -98
  107. django_cfg/modules/django_unfold/templates/unfold/helpers/app_list.html +0 -102
  108. django_cfg/static/frontend/admin/_next/static/chunks/23004-faae121bbfecc163.js +0 -1
  109. django_cfg/static/frontend/admin/_next/static/chunks/50314-48bd5701f62faf27.js +0 -1
  110. django_cfg/static/frontend/admin/_next/static/chunks/pages/private-fe9faa86ecdb0ce6.js +0 -1
  111. django_cfg/static/frontend/admin/_next/static/css/5f9a37b6e6a72303.css +0 -3
  112. django_cfg/static/frontend/admin/_next/static/media/438aa629764e75f3-s.woff2 +0 -0
  113. django_cfg/static/frontend/admin/_next/static/media/4c9affa5bc8f420e-s.p.woff2 +0 -0
  114. django_cfg/static/frontend/admin/_next/static/media/51251f8b9793cdb3-s.woff2 +0 -0
  115. django_cfg/static/frontend/admin/_next/static/media/875ae681bfde4580-s.p.woff2 +0 -0
  116. django_cfg/static/frontend/admin/_next/static/media/cc978ac5ee68c2b6-s.woff2 +0 -0
  117. django_cfg/static/frontend/admin/_next/static/media/e857b654a2caa584-s.woff2 +0 -0
  118. django_cfg/templates/admin/sections/commands_section.html +0 -5
  119. django_cfg/templates/admin/sections/documentation_section.html +0 -5
  120. django_cfg/templates/admin/sections/overview_section.html +0 -5
  121. django_cfg/templates/admin/sections/stats_section.html +0 -5
  122. django_cfg/templates/admin/sections/system_section.html +0 -5
  123. django_cfg/templates/admin/sections/widgets_section.html +0 -11
  124. django_cfg/templates/admin_old/components/action_grid.html +0 -49
  125. django_cfg/templates/admin_old/components/card.html +0 -50
  126. django_cfg/templates/admin_old/components/data_table.html +0 -67
  127. django_cfg/templates/admin_old/components/metric_card.html +0 -39
  128. django_cfg/templates/admin_old/components/modal.html +0 -58
  129. django_cfg/templates/admin_old/components/progress_bar.html +0 -20
  130. django_cfg/templates/admin_old/components/section_header.html +0 -26
  131. django_cfg/templates/admin_old/components/stat_item.html +0 -32
  132. django_cfg/templates/admin_old/components/stats_grid.html +0 -72
  133. django_cfg/templates/admin_old/components/status_badge.html +0 -28
  134. django_cfg/templates/admin_old/components/user_avatar.html +0 -27
  135. django_cfg/templates/admin_old/constance/change_list.html +0 -74
  136. django_cfg/templates/admin_old/constance/includes/default_value.html +0 -24
  137. django_cfg/templates/admin_old/constance/includes/fieldset_header.html +0 -15
  138. django_cfg/templates/admin_old/constance/includes/results_list.html +0 -16
  139. django_cfg/templates/admin_old/constance/includes/setting_row.html +0 -50
  140. django_cfg/templates/admin_old/constance/includes/table_headers.html +0 -10
  141. django_cfg/templates/admin_old/examples/component_class_example.html +0 -156
  142. django_cfg/templates/admin_old/import_export/change_list_export.html +0 -24
  143. django_cfg/templates/admin_old/import_export/change_list_import.html +0 -24
  144. django_cfg/templates/admin_old/import_export/change_list_import_export.html +0 -34
  145. django_cfg/templates/admin_old/index.html +0 -80
  146. django_cfg/templates/admin_old/index_new.html +0 -119
  147. django_cfg/templates/admin_old/layouts/base_dashboard.html +0 -62
  148. django_cfg/templates/admin_old/layouts/dashboard_with_tabs.html +0 -176
  149. django_cfg/templates/admin_old/sections/commands_section.html +0 -549
  150. django_cfg/templates/admin_old/sections/documentation_section.html +0 -152
  151. django_cfg/templates/admin_old/sections/overview_section.html +0 -112
  152. django_cfg/templates/admin_old/sections/stats_section.html +0 -35
  153. django_cfg/templates/admin_old/sections/system_section.html +0 -99
  154. django_cfg/templates/admin_old/sections/widgets_section.html +0 -129
  155. django_cfg/templates/admin_old/snippets/components/activity_tracker.html +0 -70
  156. django_cfg/templates/admin_old/snippets/components/charts_section.html +0 -113
  157. django_cfg/templates/admin_old/snippets/components/django_commands.html +0 -270
  158. django_cfg/templates/admin_old/snippets/components/quick_actions.html +0 -66
  159. django_cfg/templates/admin_old/snippets/components/recent_activity_improved.html +0 -25
  160. django_cfg/templates/admin_old/snippets/components/recent_users_table.html +0 -102
  161. django_cfg/templates/admin_old/snippets/components/stats_cards.html +0 -4
  162. django_cfg/templates/admin_old/snippets/components/stats_tiles.html +0 -92
  163. django_cfg/templates/admin_old/snippets/components/system_health.html +0 -22
  164. django_cfg/templates/admin_old/snippets/components/system_metrics.html +0 -199
  165. django_cfg/templates/admin_old/snippets/components/user_permissions.html +0 -57
  166. django_cfg/templates/admin_old/snippets/tabs/app_stats_tab.html +0 -201
  167. django_cfg/templates/admin_old/snippets/tabs/commands_tab.html +0 -114
  168. django_cfg/templates/admin_old/snippets/tabs/documentation_tab.html +0 -42
  169. django_cfg/templates/admin_old/snippets/tabs/overview_tab.html +0 -116
  170. django_cfg/templates/admin_old/snippets/tabs/stats_tab.html +0 -89
  171. django_cfg/templates/admin_old/snippets/tabs/users_tab.html +0 -51
  172. django_cfg/templates/admin_old/snippets/tabs/widgets_tab.html +0 -38
  173. django_cfg/templates/admin_old/snippets/zones/zones_table.html +0 -176
  174. /django_cfg/static/frontend/admin/_next/static/{ZJZBgOL9mO1koHrgaaLEV → 0sN9ktsgXH48ygtGSrhfu}/_ssgManifest.js +0 -0
  175. {django_cfg-1.4.87.dist-info → django_cfg-1.4.89.dist-info}/WHEEL +0 -0
  176. {django_cfg-1.4.87.dist-info → django_cfg-1.4.89.dist-info}/entry_points.txt +0 -0
  177. {django_cfg-1.4.87.dist-info → django_cfg-1.4.89.dist-info}/licenses/LICENSE +0 -0
django_cfg/__init__.py CHANGED
@@ -32,7 +32,7 @@ Example:
32
32
  default_app_config = "django_cfg.apps.DjangoCfgConfig"
33
33
 
34
34
  # Version information
35
- __version__ = "1.4.87"
35
+ __version__ = "1.4.89"
36
36
  __license__ = "MIT"
37
37
 
38
38
  # Import registry for organized lazy loading
@@ -3,7 +3,6 @@ Views for Centrifugo module.
3
3
  """
4
4
 
5
5
  from .admin_api import CentrifugoAdminAPIViewSet
6
- from .dashboard import dashboard_view
7
6
  from .monitoring import CentrifugoMonitorViewSet
8
7
  from .testing_api import CentrifugoTestingAPIViewSet
9
8
 
@@ -11,5 +10,4 @@ __all__ = [
11
10
  'CentrifugoMonitorViewSet',
12
11
  'CentrifugoAdminAPIViewSet',
13
12
  'CentrifugoTestingAPIViewSet',
14
- 'dashboard_view',
15
13
  ]
@@ -0,0 +1,48 @@
1
+ """
2
+ Dashboard Permissions
3
+
4
+ Custom permission classes for dashboard API endpoints.
5
+ """
6
+
7
+ from rest_framework.permissions import BasePermission
8
+
9
+
10
+ class IsSuperAdmin(BasePermission):
11
+ """
12
+ Permission that allows access only to superusers.
13
+
14
+ More restrictive than IsAdminUser - requires is_superuser flag.
15
+ Use for sensitive operations like command execution.
16
+ """
17
+
18
+ def has_permission(self, request, view):
19
+ """Check if user is authenticated and is a superuser."""
20
+ return bool(
21
+ request.user and
22
+ request.user.is_authenticated and
23
+ request.user.is_superuser
24
+ )
25
+
26
+
27
+ class IsStaffOrReadOnly(BasePermission):
28
+ """
29
+ Permission that allows read access to staff,
30
+ but write access only to superusers.
31
+ """
32
+
33
+ def has_permission(self, request, view):
34
+ """Check permissions based on request method."""
35
+ # Read permissions are allowed to any staff user
36
+ if request.method in ['GET', 'HEAD', 'OPTIONS']:
37
+ return bool(
38
+ request.user and
39
+ request.user.is_authenticated and
40
+ request.user.is_staff
41
+ )
42
+
43
+ # Write permissions are only allowed to superusers
44
+ return bool(
45
+ request.user and
46
+ request.user.is_authenticated and
47
+ request.user.is_superuser
48
+ )
@@ -15,7 +15,12 @@ from .charts import (
15
15
  ActivityTrackerDaySerializer,
16
16
  RecentUserSerializer,
17
17
  )
18
- from .commands import CommandSerializer, CommandsSummarySerializer
18
+ from .commands import (
19
+ CommandSerializer,
20
+ CommandsSummarySerializer,
21
+ CommandExecuteRequestSerializer,
22
+ CommandHelpResponseSerializer,
23
+ )
19
24
  from .apizones import APIZoneSerializer, APIZonesSummarySerializer
20
25
 
21
26
  __all__ = [
@@ -48,6 +53,8 @@ __all__ = [
48
53
  # Commands
49
54
  'CommandSerializer',
50
55
  'CommandsSummarySerializer',
56
+ 'CommandExecuteRequestSerializer',
57
+ 'CommandHelpResponseSerializer',
51
58
 
52
59
  # API Zones
53
60
  'APIZoneSerializer',
@@ -14,6 +14,8 @@ class CommandSerializer(serializers.Serializer):
14
14
  help = serializers.CharField()
15
15
  is_core = serializers.BooleanField()
16
16
  is_custom = serializers.BooleanField()
17
+ is_allowed = serializers.BooleanField(required=False)
18
+ risk_level = serializers.CharField(required=False)
17
19
 
18
20
 
19
21
  class CommandsSummarySerializer(serializers.Serializer):
@@ -24,3 +26,30 @@ class CommandsSummarySerializer(serializers.Serializer):
24
26
  categories = serializers.ListField(child=serializers.CharField())
25
27
  commands = CommandSerializer(many=True)
26
28
  categorized = serializers.DictField()
29
+
30
+
31
+ class CommandExecuteRequestSerializer(serializers.Serializer):
32
+ """Request serializer for command execution."""
33
+ command = serializers.CharField(help_text="Name of the Django management command")
34
+ args = serializers.ListField(
35
+ child=serializers.CharField(),
36
+ required=False,
37
+ default=list,
38
+ help_text="Positional arguments for the command"
39
+ )
40
+ options = serializers.DictField(
41
+ required=False,
42
+ default=dict,
43
+ help_text="Named options for the command (e.g., {'verbosity': '2'})"
44
+ )
45
+
46
+
47
+ class CommandHelpResponseSerializer(serializers.Serializer):
48
+ """Response serializer for command help."""
49
+ status = serializers.CharField()
50
+ command = serializers.CharField()
51
+ app = serializers.CharField(required=False)
52
+ help_text = serializers.CharField(required=False)
53
+ is_allowed = serializers.BooleanField(required=False)
54
+ risk_level = serializers.CharField(required=False)
55
+ error = serializers.CharField(required=False)
@@ -10,6 +10,7 @@ from .system_health_service import SystemHealthService
10
10
  from .charts_service import ChartsService
11
11
  from .commands_service import CommandsService
12
12
  from .apizones_service import APIZonesService
13
+ from .overview_service import OverviewService
13
14
 
14
15
  __all__ = [
15
16
  'StatisticsService',
@@ -17,4 +18,5 @@ __all__ = [
17
18
  'ChartsService',
18
19
  'CommandsService',
19
20
  'APIZonesService',
21
+ 'OverviewService',
20
22
  ]
@@ -1,5 +1,10 @@
1
1
  """
2
- Base utilities and helper functions for callbacks.
2
+ Commands Security Module
3
+
4
+ Self-contained security logic for Django management commands execution.
5
+ Determines which commands are safe to execute via web interface.
6
+
7
+ TAGS: commands, security, validation, safety
3
8
  """
4
9
 
5
10
  import importlib
@@ -7,8 +12,6 @@ import inspect
7
12
  import logging
8
13
  from typing import Any, Dict, Set
9
14
 
10
- from django.contrib.auth import get_user_model
11
- from django.core.management import get_commands
12
15
  from django.core.management.base import BaseCommand
13
16
 
14
17
  logger = logging.getLogger(__name__)
@@ -108,7 +111,7 @@ def analyze_command_safety(command_class: BaseCommand, command_name: str) -> Dic
108
111
 
109
112
  def is_command_allowed(command_name: str, app_name: str) -> bool:
110
113
  """
111
- Check if command should be displayed in web interface.
114
+ Check if command should be allowed for web execution.
112
115
 
113
116
  Priority:
114
117
  1. Custom blacklist from settings (DJANGO_CFG_COMMANDS_BLACKLIST)
@@ -136,15 +139,18 @@ def is_command_allowed(command_name: str, app_name: str) -> bool:
136
139
  # Custom blacklist from settings (highest priority)
137
140
  custom_blacklist = getattr(settings, 'DJANGO_CFG_COMMANDS_BLACKLIST', set())
138
141
  if command_name in custom_blacklist:
142
+ logger.info(f"Command {command_name} blocked by custom blacklist")
139
143
  return False
140
144
 
141
145
  # Absolute blacklist
142
146
  if command_name in ABSOLUTE_BLACKLIST:
147
+ logger.info(f"Command {command_name} blocked by absolute blacklist")
143
148
  return False
144
149
 
145
150
  # Custom whitelist from settings
146
151
  custom_whitelist = getattr(settings, 'DJANGO_CFG_COMMANDS_WHITELIST', set())
147
152
  if command_name in custom_whitelist:
153
+ logger.info(f"Command {command_name} allowed by custom whitelist")
148
154
  return True
149
155
 
150
156
  # Load and analyze command
@@ -192,99 +198,31 @@ def is_command_allowed(command_name: str, app_name: str) -> bool:
192
198
 
193
199
  # Exclude other Django core by default
194
200
  if app_name.startswith('django.'):
201
+ logger.debug(f"Command {command_name} excluded (Django core, not in safe list)")
195
202
  return False
196
203
 
197
204
  # Third-party apps - only whitelisted
205
+ logger.debug(f"Command {command_name} excluded (third-party, not whitelisted)")
198
206
  return False
199
207
 
200
208
 
201
- def get_available_commands():
202
- """Get all available Django management commands (filtered for safety)."""
203
- commands_dict = get_commands()
204
- commands_list = []
205
-
206
- for command_name, app_name in commands_dict.items():
207
- # Filter out unsafe/unwanted commands
208
- if not is_command_allowed(command_name, app_name):
209
- continue
210
-
211
- try:
212
- # Try to get command description
213
- if app_name == 'django_cfg':
214
- module_path = f'django_cfg.management.commands.{command_name}'
215
- else:
216
- module_path = f'{app_name}.management.commands.{command_name}'
217
-
218
- try:
219
- command_module = importlib.import_module(module_path)
220
- if hasattr(command_module, 'Command'):
221
- command_class = command_module.Command
222
- description = getattr(command_class, 'help', f'{command_name} command')
223
- else:
224
- description = f'{command_name} command'
225
- except ImportError:
226
- description = f'{command_name} command'
227
-
228
- commands_list.append({
229
- 'name': command_name,
230
- 'app': app_name,
231
- 'description': description,
232
- 'is_core': app_name.startswith('django.'),
233
- 'is_custom': app_name == 'django_cfg',
234
- })
235
- except Exception as e:
236
- # Skip problematic commands
237
- logger.debug(f"Skipping command {command_name}: {e}")
238
- continue
239
-
240
- return commands_list
241
-
242
-
243
- def get_commands_by_category():
244
- """Get commands categorized by type."""
245
- commands = get_available_commands()
246
-
247
- categorized = {
248
- 'django_cfg': [],
249
- 'django_core': [],
250
- 'third_party': [],
251
- 'project': [],
252
- }
209
+ def get_command_risk_level(command_name: str, app_name: str) -> str:
210
+ """
211
+ Get risk level for a command.
253
212
 
254
- for cmd in commands:
255
- if cmd['app'] == 'django_cfg':
256
- categorized['django_cfg'].append(cmd)
257
- elif cmd['app'].startswith('django.'):
258
- categorized['django_core'].append(cmd)
259
- elif cmd['app'].startswith(('src.', 'api.', 'accounts.')):
260
- categorized['project'].append(cmd)
261
- else:
262
- categorized['third_party'].append(cmd)
213
+ Returns:
214
+ 'safe', 'caution', or 'dangerous'
215
+ """
216
+ if command_name in ABSOLUTE_BLACKLIST:
217
+ return 'dangerous'
263
218
 
264
- return categorized
219
+ if app_name == 'django_cfg':
220
+ return 'safe'
265
221
 
222
+ if app_name.startswith('django.'):
223
+ safe_commands = {'check', 'diffsettings', 'showmigrations'}
224
+ if command_name in safe_commands:
225
+ return 'safe'
226
+ return 'caution'
266
227
 
267
- def get_user_admin_urls():
268
- """Get admin URLs for user model."""
269
- try:
270
- User = get_user_model()
271
-
272
- app_label = User._meta.app_label
273
- model_name = User._meta.model_name
274
-
275
- return {
276
- 'changelist': f'admin:{app_label}_{model_name}_changelist',
277
- 'add': f'admin:{app_label}_{model_name}_add',
278
- 'change': f'admin:{app_label}_{model_name}_change/{{id}}/',
279
- 'delete': f'admin:{app_label}_{model_name}_delete/{{id}}/',
280
- 'view': f'admin:{app_label}_{model_name}_view/{{id}}/',
281
- }
282
- except Exception:
283
- # Universal fallback - return admin index for all actions
284
- return {
285
- 'changelist': 'admin:index',
286
- 'add': 'admin:index',
287
- 'change': 'admin:index',
288
- 'delete': 'admin:index',
289
- 'view': 'admin:index',
290
- }
228
+ return 'caution'
@@ -1,13 +1,18 @@
1
1
  """
2
2
  Commands Service
3
3
 
4
- Django management commands discovery and documentation.
4
+ Django management commands discovery, documentation, and execution.
5
5
  """
6
6
 
7
7
  import logging
8
- from typing import Any, Dict, List
8
+ import sys
9
+ import time
10
+ from io import StringIO
11
+ from typing import Any, Dict, Generator, List
9
12
 
10
- from django.core.management import get_commands, load_command_class
13
+ from django.core.management import call_command, get_commands, load_command_class
14
+
15
+ from .commands_security import is_command_allowed, get_command_risk_level
11
16
 
12
17
  logger = logging.getLogger(__name__)
13
18
 
@@ -26,20 +31,29 @@ class CommandsService:
26
31
  """Initialize commands service."""
27
32
  self.logger = logger
28
33
 
29
- def get_all_commands(self) -> List[Dict[str, Any]]:
34
+ def get_all_commands(self, include_unsafe: bool = False) -> List[Dict[str, Any]]:
30
35
  """
31
- Get all available Django management commands.
36
+ Get all available Django management commands (filtered for safety by default).
37
+
38
+ Args:
39
+ include_unsafe: If True, include all commands. If False, only safe commands.
32
40
 
33
41
  Returns:
34
- List of command dictionaries with name, app, help text
42
+ List of command dictionaries with name, app, help text, safety info
35
43
 
36
- %%AI_HINT: Uses Django's get_commands() for command discovery%%
44
+ %%AI_HINT: Uses Django's get_commands() for command discovery with security filtering%%
37
45
  """
38
46
  try:
39
47
  commands_dict = get_commands()
40
48
  commands_list = []
41
49
 
42
50
  for command_name, app_name in commands_dict.items():
51
+ # Check if command is allowed (unless explicitly including unsafe)
52
+ is_allowed = is_command_allowed(command_name, app_name)
53
+
54
+ if not include_unsafe and not is_allowed:
55
+ continue
56
+
43
57
  try:
44
58
  # Try to load command to get help text
45
59
  command = load_command_class(app_name, command_name)
@@ -47,7 +61,10 @@ class CommandsService:
47
61
 
48
62
  # Determine if it's a core Django command or custom
49
63
  is_core = app_name.startswith('django.')
50
- is_custom = not is_core
64
+ is_custom = app_name == 'django_cfg'
65
+
66
+ # Get risk level
67
+ risk_level = get_command_risk_level(command_name, app_name)
51
68
 
52
69
  commands_list.append({
53
70
  'name': command_name,
@@ -55,6 +72,8 @@ class CommandsService:
55
72
  'help': help_text,
56
73
  'is_core': is_core,
57
74
  'is_custom': is_custom,
75
+ 'is_allowed': is_allowed,
76
+ 'risk_level': risk_level,
58
77
  })
59
78
  except Exception as e:
60
79
  # If we can't load the command, still include basic info
@@ -64,7 +83,9 @@ class CommandsService:
64
83
  'app': app_name,
65
84
  'help': 'Description unavailable',
66
85
  'is_core': app_name.startswith('django.'),
67
- 'is_custom': not app_name.startswith('django.'),
86
+ 'is_custom': app_name == 'django_cfg',
87
+ 'is_allowed': is_allowed,
88
+ 'risk_level': get_command_risk_level(command_name, app_name),
68
89
  })
69
90
 
70
91
  # Sort by name
@@ -140,3 +161,181 @@ class CommandsService:
140
161
  'commands': [],
141
162
  'categorized': {},
142
163
  }
164
+
165
+ def get_command_help(self, command_name: str) -> Dict[str, Any]:
166
+ """
167
+ Get detailed help text for a specific command.
168
+
169
+ Args:
170
+ command_name: Name of the Django management command
171
+
172
+ Returns:
173
+ Dictionary with help text and command info
174
+ """
175
+ try:
176
+ commands_dict = get_commands()
177
+
178
+ if command_name not in commands_dict:
179
+ return {
180
+ 'status': 'error',
181
+ 'error': f"Command '{command_name}' not found",
182
+ }
183
+
184
+ app_name = commands_dict[command_name]
185
+
186
+ # Load command class
187
+ command_class = load_command_class(app_name, command_name)
188
+ command_instance = command_class()
189
+
190
+ # Get parser to extract help
191
+ parser = command_instance.create_parser('manage.py', command_name)
192
+ help_text = parser.format_help()
193
+
194
+ return {
195
+ 'status': 'success',
196
+ 'command': command_name,
197
+ 'app': app_name,
198
+ 'help_text': help_text,
199
+ 'is_allowed': is_command_allowed(command_name, app_name),
200
+ 'risk_level': get_command_risk_level(command_name, app_name),
201
+ }
202
+
203
+ except Exception as e:
204
+ self.logger.error(f"Error getting help for command {command_name}: {e}")
205
+ return {
206
+ 'status': 'error',
207
+ 'error': str(e),
208
+ 'command': command_name,
209
+ }
210
+
211
+ def execute_command(
212
+ self,
213
+ command_name: str,
214
+ args: List[str] = None,
215
+ options: Dict[str, Any] = None,
216
+ user=None
217
+ ) -> Generator[Dict[str, Any], None, None]:
218
+ """
219
+ Execute a Django management command and stream output.
220
+
221
+ Args:
222
+ command_name: Name of the command to execute
223
+ args: List of positional arguments
224
+ options: Dictionary of command options
225
+ user: User executing the command (for logging)
226
+
227
+ Yields:
228
+ Dict with execution events (start, output, complete, error)
229
+
230
+ Security:
231
+ - Validates command is allowed via is_command_allowed()
232
+ - Logs all execution attempts
233
+ - Captures and streams output safely
234
+ """
235
+ args = args or []
236
+ options = options or {}
237
+ start_time = time.time()
238
+
239
+ try:
240
+ # Validate command exists
241
+ commands_dict = get_commands()
242
+ if command_name not in commands_dict:
243
+ yield {
244
+ 'type': 'error',
245
+ 'error': f"Command '{command_name}' not found",
246
+ 'available_commands': list(commands_dict.keys())[:20], # First 20 for reference
247
+ }
248
+ return
249
+
250
+ app_name = commands_dict[command_name]
251
+
252
+ # Security check - validate command is allowed
253
+ if not is_command_allowed(command_name, app_name):
254
+ self.logger.warning(
255
+ f"Attempted execution of forbidden command '{command_name}' by user {user}"
256
+ )
257
+ yield {
258
+ 'type': 'error',
259
+ 'error': f"Command '{command_name}' is not allowed via web interface for security reasons",
260
+ 'suggestion': 'Only safe django_cfg commands and whitelisted utilities can be executed via web.',
261
+ 'risk_level': get_command_risk_level(command_name, app_name),
262
+ }
263
+ return
264
+
265
+ # Send start event
266
+ yield {
267
+ 'type': 'start',
268
+ 'command': command_name,
269
+ 'args': args,
270
+ 'options': options,
271
+ }
272
+
273
+ # Capture command output
274
+ output_buffer = StringIO()
275
+ old_stdout = sys.stdout
276
+ old_stderr = sys.stderr
277
+
278
+ try:
279
+ # Redirect stdout/stderr to buffer
280
+ sys.stdout = output_buffer
281
+ sys.stderr = output_buffer
282
+
283
+ # Execute command
284
+ call_command(command_name, *args, **options)
285
+ return_code = 0
286
+
287
+ except Exception as e:
288
+ # Command execution failed
289
+ output_buffer.write(f"\nError: {str(e)}\n")
290
+ return_code = 1
291
+
292
+ finally:
293
+ # Restore stdout/stderr
294
+ sys.stdout = old_stdout
295
+ sys.stderr = old_stderr
296
+
297
+ # Get all output
298
+ output = output_buffer.getvalue()
299
+
300
+ # Stream output line by line
301
+ if output:
302
+ for line in output.split('\n'):
303
+ if line.strip():
304
+ yield {
305
+ 'type': 'output',
306
+ 'line': line,
307
+ }
308
+
309
+ execution_time = time.time() - start_time
310
+
311
+ # Send completion event
312
+ yield {
313
+ 'type': 'complete',
314
+ 'return_code': return_code,
315
+ 'execution_time': round(execution_time, 2),
316
+ }
317
+
318
+ # Log command execution
319
+ if return_code == 0:
320
+ self.logger.info(
321
+ f"Command executed: {command_name} {' '.join(args)} "
322
+ f"by user {user} in {execution_time:.2f}s"
323
+ )
324
+ else:
325
+ self.logger.error(
326
+ f"Command failed: {command_name} {' '.join(args)} "
327
+ f"by user {user} in {execution_time:.2f}s"
328
+ )
329
+
330
+ except Exception as e:
331
+ execution_time = time.time() - start_time
332
+
333
+ self.logger.error(
334
+ f"Command execution error: {command_name} by user {user}: {e}"
335
+ )
336
+
337
+ yield {
338
+ 'type': 'error',
339
+ 'error': str(e),
340
+ 'execution_time': round(execution_time, 2),
341
+ }