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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/centrifugo/views/__init__.py +0 -2
- django_cfg/apps/dashboard/permissions.py +48 -0
- django_cfg/apps/dashboard/serializers/__init__.py +8 -1
- django_cfg/apps/dashboard/serializers/commands.py +29 -0
- django_cfg/apps/dashboard/services/__init__.py +2 -0
- django_cfg/{modules/django_unfold/callbacks/base.py → apps/dashboard/services/commands_security.py} +28 -90
- django_cfg/apps/dashboard/services/commands_service.py +208 -9
- django_cfg/apps/dashboard/services/overview_service.py +205 -0
- django_cfg/apps/dashboard/views/commands_views.py +92 -4
- django_cfg/apps/frontend/test_routing.py +134 -0
- django_cfg/apps/frontend/views.py +73 -28
- django_cfg/apps/urls.py +0 -1
- django_cfg/core/builders/apps_builder.py +0 -58
- django_cfg/modules/django_unfold/__init__.py +5 -24
- django_cfg/modules/django_unfold/models/__init__.py +0 -23
- django_cfg/modules/django_unfold/models/config.py +11 -65
- django_cfg/modules/django_unfold/{dashboard.py → navigation.py} +21 -152
- django_cfg/modules/django_unfold/tailwind.py +2 -4
- django_cfg/pyproject.toml +1 -1
- django_cfg/registry/third_party.py +0 -9
- django_cfg/routing/callbacks.py +1 -43
- django_cfg/static/frontend/admin/404/index.html +1 -1
- django_cfg/static/frontend/admin/404.html +1 -1
- django_cfg/static/frontend/admin/500/index.html +1 -1
- django_cfg/static/frontend/admin/_next/static/{ZJZBgOL9mO1koHrgaaLEV → 0sN9ktsgXH48ygtGSrhfu}/_buildManifest.js +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/{19430.fe7bff7372f8a256.js → 19430.c4c95603c23c17fe.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/50314-9443faa6df24aebf.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/94141-bc6d47f419b26b21.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/pages/{_app-c336f254967dd101.js → _app-c7dcd3aa616fab68.js} +6 -6
- django_cfg/static/frontend/admin/_next/static/chunks/pages/legal/{cookies-b39c7f22c066e2c6.js → cookies-97d279800f12aab4.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/legal/{privacy-5aedad0cf3a4f80f.js → privacy-1d5e6cd94689247e.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/legal/{security-dbd854d0d5d483e2.js → security-55e49700e7a01f5a.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/legal/{terms-f3e1d2b9e5edf12f.js → terms-14c02bb2d3198352.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/private/{centrifugo-22532c65971225eb.js → centrifugo-f9ecbc3ae0052a03.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/private-d4ccbe1265cbd853.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/{webpack-da114020a6b940f5.js → webpack-5a92f81363b62aa7.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/css/3063068f0d5a8a00.css +3 -0
- django_cfg/static/frontend/admin/_next/static/media/19cfc7226ec3afaa-s.woff2 +0 -0
- django_cfg/static/frontend/admin/_next/static/media/21350d82a1f187e9-s.p.woff2 +0 -0
- django_cfg/static/frontend/admin/_next/static/media/8e9860b6e62d6359-s.woff2 +0 -0
- django_cfg/static/frontend/admin/_next/static/media/ba9851c3c22cd980-s.woff2 +0 -0
- django_cfg/static/frontend/admin/_next/static/media/c5fe6dc8356a8c31-s.woff2 +0 -0
- django_cfg/static/frontend/admin/_next/static/media/df0a9ae256c0569c-s.woff2 +0 -0
- django_cfg/static/frontend/admin/_next/static/media/e4af272ccee01ff0-s.p.woff2 +0 -0
- django_cfg/static/frontend/admin/auth/index.html +1 -1
- django_cfg/static/frontend/admin/index.html +1 -1
- django_cfg/static/frontend/admin/legal/cookies/index.html +1 -1
- django_cfg/static/frontend/admin/legal/privacy/index.html +1 -1
- django_cfg/static/frontend/admin/legal/security/index.html +1 -1
- django_cfg/static/frontend/admin/legal/terms/index.html +1 -1
- django_cfg/static/frontend/admin/private/centrifugo/index.html +1 -1
- django_cfg/static/frontend/admin/private/index.html +1 -1
- django_cfg/static/frontend/admin/private/profile/index.html +1 -1
- django_cfg/static/frontend/admin/private/ui/index.html +2 -2
- django_cfg/templates/admin/index.html +1 -1
- {django_cfg-1.4.87.dist-info → django_cfg-1.4.89.dist-info}/METADATA +1 -1
- {django_cfg-1.4.87.dist-info → django_cfg-1.4.89.dist-info}/RECORD +62 -163
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/css/dashboard.css +0 -260
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/live_channels.mjs +0 -313
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/live_testing.mjs +0 -803
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/main.mjs +0 -341
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/overview.mjs +0 -432
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/testing.mjs +0 -33
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/websocket.mjs +0 -210
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/channels_content.html +0 -46
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/live_channels_content.html +0 -123
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/overview_content.html +0 -45
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/publishes_content.html +0 -84
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/stat_cards.html +0 -53
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/system_status.html +0 -91
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/tab_navigation.html +0 -29
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/testing_tools.html +0 -415
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/layout/base.html +0 -61
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/pages/dashboard.html +0 -58
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/tags/connection_script.html +0 -48
- django_cfg/apps/centrifugo/templatetags/__init__.py +0 -1
- django_cfg/apps/centrifugo/templatetags/centrifugo_tags.py +0 -81
- django_cfg/apps/centrifugo/urls_admin.py +0 -20
- django_cfg/apps/centrifugo/views/dashboard.py +0 -28
- django_cfg/modules/django_dashboard/__init__.py +0 -23
- django_cfg/modules/django_dashboard/components.py +0 -312
- django_cfg/modules/django_dashboard/debug.py +0 -174
- django_cfg/modules/django_dashboard/management/__init__.py +0 -0
- django_cfg/modules/django_dashboard/management/commands/__init__.py +0 -0
- django_cfg/modules/django_dashboard/management/commands/debug_dashboard.py +0 -109
- django_cfg/modules/django_dashboard/sections/__init__.py +0 -1
- django_cfg/modules/django_dashboard/sections/base.py +0 -129
- django_cfg/modules/django_dashboard/sections/commands.py +0 -33
- django_cfg/modules/django_dashboard/sections/documentation.py +0 -393
- django_cfg/modules/django_dashboard/sections/overview.py +0 -398
- django_cfg/modules/django_dashboard/sections/stats.py +0 -48
- django_cfg/modules/django_dashboard/sections/system.py +0 -74
- django_cfg/modules/django_dashboard/sections/widgets.py +0 -222
- django_cfg/modules/django_unfold/callbacks/__init__.py +0 -9
- django_cfg/modules/django_unfold/callbacks/actions.py +0 -51
- django_cfg/modules/django_unfold/callbacks/apizones.py +0 -122
- django_cfg/modules/django_unfold/callbacks/charts.py +0 -223
- django_cfg/modules/django_unfold/callbacks/commands.py +0 -40
- django_cfg/modules/django_unfold/callbacks/main.py +0 -322
- django_cfg/modules/django_unfold/callbacks/statistics.py +0 -240
- django_cfg/modules/django_unfold/callbacks/system.py +0 -180
- django_cfg/modules/django_unfold/callbacks/users.py +0 -65
- django_cfg/modules/django_unfold/models/dashboard.py +0 -207
- django_cfg/modules/django_unfold/models/tabs.py +0 -26
- django_cfg/modules/django_unfold/models.py +0 -98
- django_cfg/modules/django_unfold/templates/unfold/helpers/app_list.html +0 -102
- django_cfg/static/frontend/admin/_next/static/chunks/23004-faae121bbfecc163.js +0 -1
- django_cfg/static/frontend/admin/_next/static/chunks/50314-48bd5701f62faf27.js +0 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/private-fe9faa86ecdb0ce6.js +0 -1
- django_cfg/static/frontend/admin/_next/static/css/5f9a37b6e6a72303.css +0 -3
- django_cfg/static/frontend/admin/_next/static/media/438aa629764e75f3-s.woff2 +0 -0
- django_cfg/static/frontend/admin/_next/static/media/4c9affa5bc8f420e-s.p.woff2 +0 -0
- django_cfg/static/frontend/admin/_next/static/media/51251f8b9793cdb3-s.woff2 +0 -0
- django_cfg/static/frontend/admin/_next/static/media/875ae681bfde4580-s.p.woff2 +0 -0
- django_cfg/static/frontend/admin/_next/static/media/cc978ac5ee68c2b6-s.woff2 +0 -0
- django_cfg/static/frontend/admin/_next/static/media/e857b654a2caa584-s.woff2 +0 -0
- django_cfg/templates/admin/sections/commands_section.html +0 -5
- django_cfg/templates/admin/sections/documentation_section.html +0 -5
- django_cfg/templates/admin/sections/overview_section.html +0 -5
- django_cfg/templates/admin/sections/stats_section.html +0 -5
- django_cfg/templates/admin/sections/system_section.html +0 -5
- django_cfg/templates/admin/sections/widgets_section.html +0 -11
- django_cfg/templates/admin_old/components/action_grid.html +0 -49
- django_cfg/templates/admin_old/components/card.html +0 -50
- django_cfg/templates/admin_old/components/data_table.html +0 -67
- django_cfg/templates/admin_old/components/metric_card.html +0 -39
- django_cfg/templates/admin_old/components/modal.html +0 -58
- django_cfg/templates/admin_old/components/progress_bar.html +0 -20
- django_cfg/templates/admin_old/components/section_header.html +0 -26
- django_cfg/templates/admin_old/components/stat_item.html +0 -32
- django_cfg/templates/admin_old/components/stats_grid.html +0 -72
- django_cfg/templates/admin_old/components/status_badge.html +0 -28
- django_cfg/templates/admin_old/components/user_avatar.html +0 -27
- django_cfg/templates/admin_old/constance/change_list.html +0 -74
- django_cfg/templates/admin_old/constance/includes/default_value.html +0 -24
- django_cfg/templates/admin_old/constance/includes/fieldset_header.html +0 -15
- django_cfg/templates/admin_old/constance/includes/results_list.html +0 -16
- django_cfg/templates/admin_old/constance/includes/setting_row.html +0 -50
- django_cfg/templates/admin_old/constance/includes/table_headers.html +0 -10
- django_cfg/templates/admin_old/examples/component_class_example.html +0 -156
- django_cfg/templates/admin_old/import_export/change_list_export.html +0 -24
- django_cfg/templates/admin_old/import_export/change_list_import.html +0 -24
- django_cfg/templates/admin_old/import_export/change_list_import_export.html +0 -34
- django_cfg/templates/admin_old/index.html +0 -80
- django_cfg/templates/admin_old/index_new.html +0 -119
- django_cfg/templates/admin_old/layouts/base_dashboard.html +0 -62
- django_cfg/templates/admin_old/layouts/dashboard_with_tabs.html +0 -176
- django_cfg/templates/admin_old/sections/commands_section.html +0 -549
- django_cfg/templates/admin_old/sections/documentation_section.html +0 -152
- django_cfg/templates/admin_old/sections/overview_section.html +0 -112
- django_cfg/templates/admin_old/sections/stats_section.html +0 -35
- django_cfg/templates/admin_old/sections/system_section.html +0 -99
- django_cfg/templates/admin_old/sections/widgets_section.html +0 -129
- django_cfg/templates/admin_old/snippets/components/activity_tracker.html +0 -70
- django_cfg/templates/admin_old/snippets/components/charts_section.html +0 -113
- django_cfg/templates/admin_old/snippets/components/django_commands.html +0 -270
- django_cfg/templates/admin_old/snippets/components/quick_actions.html +0 -66
- django_cfg/templates/admin_old/snippets/components/recent_activity_improved.html +0 -25
- django_cfg/templates/admin_old/snippets/components/recent_users_table.html +0 -102
- django_cfg/templates/admin_old/snippets/components/stats_cards.html +0 -4
- django_cfg/templates/admin_old/snippets/components/stats_tiles.html +0 -92
- django_cfg/templates/admin_old/snippets/components/system_health.html +0 -22
- django_cfg/templates/admin_old/snippets/components/system_metrics.html +0 -199
- django_cfg/templates/admin_old/snippets/components/user_permissions.html +0 -57
- django_cfg/templates/admin_old/snippets/tabs/app_stats_tab.html +0 -201
- django_cfg/templates/admin_old/snippets/tabs/commands_tab.html +0 -114
- django_cfg/templates/admin_old/snippets/tabs/documentation_tab.html +0 -42
- django_cfg/templates/admin_old/snippets/tabs/overview_tab.html +0 -116
- django_cfg/templates/admin_old/snippets/tabs/stats_tab.html +0 -89
- django_cfg/templates/admin_old/snippets/tabs/users_tab.html +0 -51
- django_cfg/templates/admin_old/snippets/tabs/widgets_tab.html +0 -38
- django_cfg/templates/admin_old/snippets/zones/zones_table.html +0 -176
- /django_cfg/static/frontend/admin/_next/static/{ZJZBgOL9mO1koHrgaaLEV → 0sN9ktsgXH48ygtGSrhfu}/_ssgManifest.js +0 -0
- {django_cfg-1.4.87.dist-info → django_cfg-1.4.89.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.87.dist-info → django_cfg-1.4.89.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.87.dist-info → django_cfg-1.4.89.dist-info}/licenses/LICENSE +0 -0
django_cfg/__init__.py
CHANGED
|
@@ -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
|
|
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
|
]
|
django_cfg/{modules/django_unfold/callbacks/base.py → apps/dashboard/services/commands_security.py}
RENAMED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
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
|
|
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
|
|
202
|
-
"""
|
|
203
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
4
|
+
Django management commands discovery, documentation, and execution.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
|
-
|
|
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 =
|
|
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':
|
|
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
|
+
}
|