mirror of https://github.com/jumpserver/jumpserver
Fix rbac (#7728)
* perf: 重命名 signal handlers * fix: 修复 ticket processor 问题 * perf: 修改 ticket 处理人api * fix: 修复创建系统账号bug * fix: 升级celery_beat==2.2.1和flower==1.0.0;修改celery进程启动参数先后顺序 * perf: 修改 authentication token * fix: 修复上传权限bug * fix: 登录页面增加i18n切换; * fix: 系统角色删除限制 * perf: 修改一下 permissions tree * perf: 生成 i18n * perf: 修改一点点 Co-authored-by: ibuler <ibuler@qq.com> Co-authored-by: feng626 <1304903146@qq.com> Co-authored-by: Jiangjie.Bai <bugatti_it@163.com>pull/7729/head
parent
04e46e4b1c
commit
dafc416783
|
@ -53,7 +53,7 @@ class CommandConfirmAPI(CreateAPIView):
|
|||
run_command=self.serializer.data.get('run_command'),
|
||||
session=self.serializer.session,
|
||||
cmd_filter_rule=self.serializer.cmd_filter_rule,
|
||||
org_id=self.serializer.org.id
|
||||
org_id=self.serializer.org.id,
|
||||
)
|
||||
return ticket
|
||||
|
||||
|
|
|
@ -6,11 +6,11 @@ from django.apps import AppConfig
|
|||
|
||||
class AssetsConfig(AppConfig):
|
||||
name = 'assets'
|
||||
verbose_name = _('Assets')
|
||||
verbose_name = _('App assets')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def ready(self):
|
||||
super().ready()
|
||||
from . import signals_handler
|
||||
from . import signal_handlers
|
||||
|
|
|
@ -355,6 +355,6 @@ class Asset(AbsConnectivity, AbsHardwareInfo, ProtocolsMixin, NodesRelationMixin
|
|||
verbose_name = _("Asset")
|
||||
ordering = ["hostname", ]
|
||||
permissions = [
|
||||
('test_assetconnectivity', 'Can test asset connectivity'),
|
||||
('push_assetsystemuser', 'Can push system user to asset'),
|
||||
('test_assetconnectivity', _('Can test asset connectivity')),
|
||||
('push_assetsystemuser', _('Can push system user to asset')),
|
||||
]
|
||||
|
|
|
@ -9,6 +9,6 @@ class AuditsConfig(AppConfig):
|
|||
verbose_name = _('Audits')
|
||||
|
||||
def ready(self):
|
||||
from . import signals_handler
|
||||
from . import signal_handlers
|
||||
if settings.SYSLOG_ENABLE:
|
||||
post_save.connect(signals_handler.on_audits_log_create)
|
||||
|
|
|
@ -302,16 +302,26 @@ class SecretDetailMixin:
|
|||
user=user, system_user=system_user,
|
||||
expired_at=expired_at, actions=actions
|
||||
)
|
||||
cmd_filter_kwargs = {
|
||||
'system_user_id': system_user.id,
|
||||
'user_id': user.id,
|
||||
}
|
||||
if asset:
|
||||
asset_detail = self._get_asset_secret_detail(asset)
|
||||
system_user.load_asset_more_auth(asset.id, user.username, user.id)
|
||||
data['type'] = 'asset'
|
||||
data.update(asset_detail)
|
||||
cmd_filter_kwargs['asset_id'] = asset.id
|
||||
else:
|
||||
app_detail = self._get_application_secret_detail(app)
|
||||
system_user.load_app_more_auth(app.id, user.username, user.id)
|
||||
data['type'] = 'application'
|
||||
data.update(app_detail)
|
||||
cmd_filter_kwargs['application_id'] = app.id
|
||||
|
||||
from assets.models import CommandFilterRule
|
||||
cmd_filter_rules = CommandFilterRule.get_queryset(**cmd_filter_kwargs)
|
||||
data['cmd_filter_rules'] = cmd_filter_rules
|
||||
|
||||
serializer = self.get_serializer(data)
|
||||
return Response(data=serializer.data, status=200)
|
||||
|
@ -350,8 +360,10 @@ class UserConnectionTokenViewSet(
|
|||
return True
|
||||
|
||||
def create_token(self, user, asset, application, system_user, ttl=5 * 60):
|
||||
if not self.request.user.is_superuser and user != self.request.user:
|
||||
raise PermissionDenied('Only super user can create user token')
|
||||
# 再次强调一下权限
|
||||
perm_required = 'authentication.add_superconnectiontoken'
|
||||
if user != self.request.user and not self.request.user.has_perm(perm_required):
|
||||
raise PermissionDenied('Only can create user token')
|
||||
self.check_resource_permission(user, asset, application, system_user)
|
||||
token = random_string(36)
|
||||
secret = random_string(16)
|
||||
|
|
|
@ -7,7 +7,7 @@ class AuthenticationConfig(AppConfig):
|
|||
verbose_name = _('Authentication')
|
||||
|
||||
def ready(self):
|
||||
from . import signals_handlers
|
||||
from . import signal_handlers
|
||||
from . import notifications
|
||||
super().ready()
|
||||
|
||||
|
|
|
@ -19,8 +19,14 @@ class Migration(migrations.Migration):
|
|||
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
|
||||
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||
],
|
||||
options={
|
||||
'permissions': [('add_superconnectiontoken', 'Can add super connection token'), ('view_connectiontokensecret', 'Can view connect token secret')],
|
||||
},
|
||||
options={'verbose_name': 'Connection token'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='accesskey',
|
||||
options={'verbose_name': 'Access key'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='ssotoken',
|
||||
options={'verbose_name': 'SSO token'},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
# Generated by Django 3.1.13 on 2022-02-17 13:35
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('authentication', '0007_connectiontoken'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='accesskey',
|
||||
options={'verbose_name': 'Access key'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='ssotoken',
|
||||
options={'verbose_name': 'SSO token'},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,25 @@
|
|||
# Generated by Django 3.1.14 on 2022-03-02 11:53
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('authentication', '0007_connectiontoken'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SuperConnectionToken',
|
||||
fields=[
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Super connection token',
|
||||
'proxy': True,
|
||||
'indexes': [],
|
||||
'constraints': [],
|
||||
},
|
||||
bases=('authentication.connectiontoken',),
|
||||
),
|
||||
]
|
|
@ -58,7 +58,10 @@ class ConnectionToken(models.JMSBaseModel):
|
|||
# Todo: add connection token 可能要授权给 普通用户, 或者放开就行
|
||||
|
||||
class Meta:
|
||||
permissions = [
|
||||
('add_superconnectiontoken', _('Can add super connection token')),
|
||||
('view_connectiontokensecret', _('Can view connect token secret'))
|
||||
]
|
||||
verbose_name = _('Connection token')
|
||||
|
||||
|
||||
class SuperConnectionToken(ConnectionToken):
|
||||
class Meta:
|
||||
proxy = True
|
||||
verbose_name = _("Super connection token")
|
||||
|
|
|
@ -5,7 +5,7 @@ from rest_framework import serializers
|
|||
|
||||
from common.utils import get_object_or_none
|
||||
from users.models import User
|
||||
from assets.models import Asset, SystemUser, Gateway, Domain
|
||||
from assets.models import Asset, SystemUser, Gateway, Domain, CommandFilterRule
|
||||
from applications.models import Application
|
||||
from users.serializers import UserProfileSerializer
|
||||
from assets.serializers import ProtocolsField
|
||||
|
@ -200,6 +200,17 @@ class ConnectionTokenDomainSerializer(serializers.ModelSerializer):
|
|||
fields = ['id', 'name', 'gateways']
|
||||
|
||||
|
||||
class ConnectionTokenFilterRuleSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = CommandFilterRule
|
||||
fields = [
|
||||
'id', 'type', 'content', 'ignore_case', 'pattern',
|
||||
'priority', 'action',
|
||||
'date_created',
|
||||
]
|
||||
|
||||
|
||||
class ConnectionTokenSecretSerializer(serializers.Serializer):
|
||||
id = serializers.CharField(read_only=True)
|
||||
secret = serializers.CharField(read_only=True)
|
||||
|
@ -209,6 +220,7 @@ class ConnectionTokenSecretSerializer(serializers.Serializer):
|
|||
remote_app = ConnectionTokenRemoteAppSerializer(read_only=True)
|
||||
application = ConnectionTokenApplicationSerializer(read_only=True)
|
||||
system_user = ConnectionTokenSystemUserSerializer(read_only=True)
|
||||
cmd_filter_rules = ConnectionTokenFilterRuleSerializer(many=True)
|
||||
domain = ConnectionTokenDomainSerializer(read_only=True)
|
||||
gateway = ConnectionTokenGatewaySerializer(read_only=True)
|
||||
actions = ActionsField()
|
||||
|
|
|
@ -115,10 +115,21 @@
|
|||
.mfa-div {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login-page-language {
|
||||
margin-right: -11px !important;
|
||||
padding-top: 12px !important;
|
||||
padding-left: 0 !important;
|
||||
padding-bottom: 8px !important;
|
||||
color: #666 !important;
|
||||
font-weight: 350 !important;
|
||||
min-height: auto !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="login-content">
|
||||
<div class="right-image-box">
|
||||
<a href="{% if not XPACK_ENABLED %}https://github.com/jumpserver/jumpserver{% endif %}">
|
||||
|
@ -127,6 +138,22 @@
|
|||
</div>
|
||||
<div class="left-form-box {% if not form.challenge and not form.captcha %} no-captcha-challenge {% endif %}">
|
||||
<div style="background-color: white">
|
||||
<ul class="nav navbar-top-links navbar-right">
|
||||
<li class="dropdown">
|
||||
<a class="dropdown-toggle login-page-language" data-toggle="dropdown" href="#" target="_blank">
|
||||
<i class="fa fa-globe fa-lg" style="margin-right: 2px"></i>
|
||||
{% ifequal request.COOKIES.django_language 'en' %}
|
||||
<span>English<b class="caret"></b></span>
|
||||
{% else %}
|
||||
<span>中文(简体)<b class="caret"></b></span>
|
||||
{% endifequal %}
|
||||
</a>
|
||||
<ul class="dropdown-menu profile-dropdown dropdown-menu-right">
|
||||
<li> <a id="switch_cn" href="{% url 'i18n-switch' lang='zh-hans' %}"> <span>中文(简体)</span> </a> </li>
|
||||
<li> <a id="switch_en" href="{% url 'i18n-switch' lang='en' %}"> <span>English</span> </a> </li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="jms-title">
|
||||
<span style="font-size: 21px;font-weight:400;color: #151515;letter-spacing: 0;">{{ JMS_TITLE }}</span>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
|
||||
from assets.signals_handler.node_assets_mapping import expire_node_assets_mapping_for_memory
|
||||
from assets.signal_handlers.node_assets_mapping import expire_node_assets_mapping_for_memory
|
||||
from orgs.caches import OrgResourceStatisticsCache
|
||||
from orgs.models import Organization
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ class Services(TextChoices):
|
|||
|
||||
@classmethod
|
||||
def export_services_values(cls):
|
||||
return [cls.all.value, cls.web.value, cls.task.value]
|
||||
return [cls.all.value, cls.web.value, cls.task.value] + [s.value for s in cls.all_services()]
|
||||
|
||||
@classmethod
|
||||
def get_service_objects(cls, service_names, **kwargs):
|
||||
|
|
|
@ -23,9 +23,10 @@ class CeleryBaseService(BaseService):
|
|||
server_hostname = '%h'
|
||||
|
||||
cmd = [
|
||||
'celery', 'worker',
|
||||
'-P', 'threads',
|
||||
'celery',
|
||||
'-A', 'ops',
|
||||
'worker',
|
||||
'-P', 'threads',
|
||||
'-l', 'INFO',
|
||||
'-c', str(self.num),
|
||||
'-Q', self.queue,
|
||||
|
|
|
@ -16,8 +16,9 @@ class FlowerService(BaseService):
|
|||
if os.getuid() == 0:
|
||||
os.environ.setdefault('C_FORCE_ROOT', '1')
|
||||
cmd = [
|
||||
'celery', 'flower',
|
||||
'celery',
|
||||
'-A', 'ops',
|
||||
'flower',
|
||||
'-l', 'INFO',
|
||||
'--url_prefix=/core/flower',
|
||||
'--auto_refresh=False',
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
#
|
||||
from django.templatetags.static import static
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
default_context = {
|
||||
'DEFAULT_PK': '00000000-0000-0000-0000-000000000000',
|
||||
|
|
|
@ -33,7 +33,8 @@ app_view_patterns = [
|
|||
path('ops/', include('ops.urls.view_urls'), name='ops'),
|
||||
path('common/', include('common.urls.view_urls'), name='common'),
|
||||
re_path(r'flower/(?P<path>.*)', views.celery_flower_view, name='flower-view'),
|
||||
path('download/', views.ResourceDownload.as_view(), name='download')
|
||||
path('download/', views.ResourceDownload.as_view(), name='download'),
|
||||
path('i18n/<str:lang>/', views.I18NView.as_view(), name='i18n-switch'),
|
||||
]
|
||||
|
||||
if settings.XPACK_ENABLED:
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6144c6aa78bcaf3c282ee63f9e48b11fb8c327192aa8ea8f1ff1c2080084304f
|
||||
size 100589
|
||||
oid sha256:8bd2394fc5d9bb9254965db4273a09d4ddabd8051b4855b9642476ff9cab836b
|
||||
size 101898
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -7,6 +7,6 @@ class NotificationsConfig(AppConfig):
|
|||
verbose_name = _('Notifications')
|
||||
|
||||
def ready(self):
|
||||
from . import signals_handler
|
||||
from . import signal_handlers
|
||||
from . import notifications
|
||||
super().ready()
|
||||
|
|
|
@ -5,7 +5,7 @@ from channels.generic.websocket import JsonWebsocketConsumer
|
|||
from common.utils import get_logger
|
||||
from common.db.utils import safe_db_connection
|
||||
from .site_msg import SiteMessageUtil
|
||||
from .signals_handler import new_site_msg_chan
|
||||
from .signal_handlers import new_site_msg_chan
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
|
|
@ -6,13 +6,13 @@ from django.apps import AppConfig
|
|||
|
||||
class OpsConfig(AppConfig):
|
||||
name = 'ops'
|
||||
verbose_name = _('Operations')
|
||||
verbose_name = _('App ops')
|
||||
|
||||
def ready(self):
|
||||
from orgs.models import Organization
|
||||
from orgs.utils import set_current_org
|
||||
set_current_org(Organization.root())
|
||||
from .celery import signal_handler
|
||||
from . import signals_handler
|
||||
from . import signal_handlers
|
||||
from . import notifications
|
||||
super().ready()
|
||||
|
|
|
@ -13,7 +13,7 @@ __all__ = ('ServerPerformanceMessage', 'ServerPerformanceCheckUtil')
|
|||
|
||||
class ServerPerformanceMessage(SystemMessage):
|
||||
category = 'Operations'
|
||||
category_label = _('Operations')
|
||||
category_label = _('App ops')
|
||||
message_type_label = _('Server performance')
|
||||
|
||||
def __init__(self, terms_with_errors):
|
||||
|
|
|
@ -4,7 +4,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||
|
||||
class OrgsConfig(AppConfig):
|
||||
name = 'orgs'
|
||||
verbose_name = _('Organizations')
|
||||
verbose_name = _('App organizations')
|
||||
|
||||
def ready(self):
|
||||
from . import signals_handler
|
||||
from . import signal_handlers
|
||||
|
|
|
@ -6,9 +6,9 @@ from django.utils.translation import ugettext_lazy as _
|
|||
|
||||
class PermsConfig(AppConfig):
|
||||
name = 'perms'
|
||||
verbose_name = _('Permissions')
|
||||
verbose_name = _('App permissions')
|
||||
|
||||
def ready(self):
|
||||
super().ready()
|
||||
from . import signals_handler
|
||||
from . import signal_handlers
|
||||
from . import notifications
|
||||
|
|
|
@ -43,7 +43,7 @@ class ApplicationPermissionSerializer(BasePermissionSerializer):
|
|||
'users_amount': {'label': _('Users amount')},
|
||||
'user_groups_amount': {'label': _('User groups amount')},
|
||||
'system_users_amount': {'label': _('System users amount')},
|
||||
'applications_amount': {'label': _('Applications amount')},
|
||||
'applications_amount': {'label': _('Apps amount')},
|
||||
}
|
||||
|
||||
def _filter_actions_choices(self, choices):
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.db.models import F, Value
|
||||
from django.db.models.functions import Concat
|
||||
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from orgs.utils import current_org
|
||||
from common.exceptions import JMSException
|
||||
from .. import serializers
|
||||
from ..models import RoleBinding, SystemRoleBinding, OrgRoleBinding
|
||||
|
||||
|
@ -22,7 +23,7 @@ class RoleBindingViewSet(OrgBulkModelViewSet):
|
|||
]
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()\
|
||||
queryset = super().get_queryset() \
|
||||
.prefetch_related('user', 'role') \
|
||||
.annotate(
|
||||
user_display=Concat(
|
||||
|
@ -38,6 +39,17 @@ class SystemRoleBindingViewSet(RoleBindingViewSet):
|
|||
model = SystemRoleBinding
|
||||
serializer_class = serializers.SystemRoleBindingSerializer
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
user = instance.user
|
||||
role_qs = self.model.objects.filter(user=user)
|
||||
if role_qs.count() == 1:
|
||||
msg = _('{} at least one system role').format(user)
|
||||
raise JMSException(
|
||||
code='system_role_delete_error',
|
||||
detail=msg
|
||||
)
|
||||
super().perform_destroy(instance)
|
||||
|
||||
|
||||
class OrgRoleBindingViewSet(RoleBindingViewSet):
|
||||
model = OrgRoleBinding
|
||||
|
|
|
@ -16,10 +16,12 @@ exclude_permissions = (
|
|||
('contenttypes', '*', '*', '*'),
|
||||
('django_cas_ng', '*', '*', '*'),
|
||||
('django_celery_beat', '*', '*', '*'),
|
||||
('jms_oidc_rp', '*', '*', '*'),
|
||||
('admin', '*', '*', '*'),
|
||||
('sessions', '*', '*', '*'),
|
||||
('notifications', '*', '*', '*'),
|
||||
|
||||
('applications', 'applicationuser', '*', '*'),
|
||||
('applications', 'historicalaccount', '*', '*'),
|
||||
('applications', 'databaseapp', '*', '*'),
|
||||
('applications', 'k8sapp', '*', '*'),
|
||||
|
@ -51,9 +53,15 @@ exclude_permissions = (
|
|||
('audits', 'userloginlog', 'change,delete,change', 'userloginlog'),
|
||||
('audits', 'ftplog', 'change,delete', 'ftplog'),
|
||||
('terminal', 'session', 'delete', 'session'),
|
||||
('terminal', 'session', 'delete,change', 'command'),
|
||||
('tickets', 'ticket', '*', '*'),
|
||||
('users', 'userpasswordhistory', '*', '*'),
|
||||
('xpack', 'interface', 'add,delete', 'interface'),
|
||||
('xpack', 'interface', '*', '*'),
|
||||
('xpack', 'license', '*', '*'),
|
||||
('common', 'permission', 'add,delete,view,change', 'permission'),
|
||||
('terminal', 'command', 'delete,change', 'command'),
|
||||
('terminal', 'sessionjoinrecord', 'delete', 'sessionjoinrecord'),
|
||||
('terminal', 'sessionreplay', 'delete', 'sessionreplay'),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
from typing import Callable
|
||||
|
||||
from django.db.models import F, Count, Q
|
||||
from django.apps import apps
|
||||
from django.utils.translation import ugettext_lazy as _, ugettext
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.contrib.auth.models import Permission as DjangoPermission
|
||||
from django.contrib.auth.models import ContentType as DjangoContentType
|
||||
|
||||
from common.tree import TreeNode
|
||||
from .. import const
|
||||
|
||||
Scope = const.Scope
|
||||
|
@ -19,190 +15,6 @@ class ContentType(DjangoContentType):
|
|||
proxy = True
|
||||
|
||||
|
||||
class PermissionTreeUtil:
|
||||
get_permissions: Callable
|
||||
|
||||
def __init__(self, permissions, scope, check_disabled=False):
|
||||
self.permissions = self.prefetch_permissions(permissions)
|
||||
self.all_permissions = self.prefetch_permissions(
|
||||
Permission.get_permissions(scope)
|
||||
)
|
||||
self.check_disabled = check_disabled
|
||||
|
||||
@staticmethod
|
||||
def prefetch_permissions(perms):
|
||||
return perms.select_related('content_type') \
|
||||
.annotate(app=F('content_type__app_label')) \
|
||||
.annotate(model=F('content_type__model'))
|
||||
|
||||
def _create_apps_tree_nodes(self):
|
||||
app_counts = self.all_permissions.values('app')\
|
||||
.order_by('app').annotate(count=Count('app'))
|
||||
app_checked_counts = self.permissions.values('app')\
|
||||
.order_by('app').annotate(count=Count('app'))
|
||||
app_checked_counts_mapper = {
|
||||
i['app']: i['count']
|
||||
for i in app_checked_counts
|
||||
}
|
||||
all_apps = apps.get_app_configs()
|
||||
apps_name_mapper = {
|
||||
app.name: app.verbose_name
|
||||
for app in all_apps if hasattr(app, 'verbose_name')
|
||||
}
|
||||
nodes = []
|
||||
|
||||
for i in app_counts:
|
||||
app = i['app']
|
||||
total_counts = i['count']
|
||||
check_counts = app_checked_counts_mapper.get(app, 0)
|
||||
name = apps_name_mapper.get(app, app)
|
||||
full_name = f'{name}({check_counts}/{total_counts})'
|
||||
|
||||
node = TreeNode(**{
|
||||
'id': app,
|
||||
'name': full_name,
|
||||
'title': name,
|
||||
'pId': '$ROOT$',
|
||||
'isParent': True,
|
||||
'open': False,
|
||||
'chkDisabled': self.check_disabled,
|
||||
'checked': total_counts == check_counts,
|
||||
'iconSkin': '',
|
||||
'meta': {
|
||||
'type': 'app',
|
||||
}
|
||||
})
|
||||
nodes.append(node)
|
||||
return nodes
|
||||
|
||||
def _create_models_tree_nodes(self):
|
||||
content_types = ContentType.objects.all()
|
||||
|
||||
model_counts = self.all_permissions \
|
||||
.values('model', 'app', 'content_type') \
|
||||
.order_by('content_type') \
|
||||
.annotate(count=Count('content_type'))
|
||||
model_check_counts = self.permissions \
|
||||
.values('content_type', 'model') \
|
||||
.order_by('content_type') \
|
||||
.annotate(count=Count('content_type'))
|
||||
model_counts_mapper = {
|
||||
i['content_type']: i['count']
|
||||
for i in model_counts
|
||||
}
|
||||
model_check_counts_mapper = {
|
||||
i['content_type']: i['count']
|
||||
for i in model_check_counts
|
||||
}
|
||||
|
||||
nodes = []
|
||||
for ct in content_types:
|
||||
total_counts = model_counts_mapper.get(ct.id, 0)
|
||||
if total_counts == 0:
|
||||
continue
|
||||
check_counts = model_check_counts_mapper.get(ct.id, 0)
|
||||
model_id = f'{ct.app_label}_{ct.model}'
|
||||
name = f'{ct.name}({check_counts}/{total_counts})'
|
||||
node = TreeNode(**{
|
||||
'id': model_id,
|
||||
'name': name,
|
||||
'title': name,
|
||||
'pId': ct.app_label,
|
||||
'chkDisabled': self.check_disabled,
|
||||
'isParent': True,
|
||||
'open': False,
|
||||
'checked': total_counts == check_counts,
|
||||
'meta': {
|
||||
'type': 'model',
|
||||
}
|
||||
})
|
||||
nodes.append(node)
|
||||
return nodes
|
||||
|
||||
@staticmethod
|
||||
def _get_permission_name(p, content_types_name_mapper):
|
||||
code_name = p.codename
|
||||
action_mapper = {
|
||||
'add': ugettext('Create'),
|
||||
'view': ugettext('View'),
|
||||
'change': ugettext('Update'),
|
||||
'delete': ugettext('Delete')
|
||||
}
|
||||
name = ''
|
||||
ct = ''
|
||||
if 'add_' in p.codename:
|
||||
name = action_mapper['add']
|
||||
ct = code_name.replace('add_', '')
|
||||
elif 'view_' in p.codename:
|
||||
name = action_mapper['view']
|
||||
ct = code_name.replace('view_', '')
|
||||
elif 'change_' in p.codename:
|
||||
name = action_mapper['change']
|
||||
ct = code_name.replace('change_', '')
|
||||
elif 'delete' in code_name:
|
||||
name = action_mapper['delete']
|
||||
ct = code_name.replace('delete_', '')
|
||||
|
||||
if ct in content_types_name_mapper:
|
||||
name += content_types_name_mapper[ct]
|
||||
else:
|
||||
name = p.name
|
||||
return name
|
||||
|
||||
def _create_perms_tree_nodes(self):
|
||||
permissions_id = self.permissions.values_list('id', flat=True)
|
||||
nodes = []
|
||||
content_types = ContentType.objects.all()
|
||||
content_types_name_mapper = {ct.model: ct.name for ct in content_types}
|
||||
for p in self.all_permissions:
|
||||
model_id = f'{p.app}_{p.model}'
|
||||
name = self._get_permission_name(p, content_types_name_mapper)
|
||||
|
||||
node = TreeNode(**{
|
||||
'id': p.id,
|
||||
'name': name + '({})'.format(p.app_label_codename),
|
||||
'title': p.name,
|
||||
'pId': model_id,
|
||||
'isParent': False,
|
||||
'chkDisabled': self.check_disabled,
|
||||
'iconSkin': 'file',
|
||||
'checked': p.id in permissions_id,
|
||||
'open': False,
|
||||
'meta': {
|
||||
'type': 'perm',
|
||||
}
|
||||
})
|
||||
nodes.append(node)
|
||||
return nodes
|
||||
|
||||
def _create_root_tree_node(self):
|
||||
total_counts = self.all_permissions.count()
|
||||
check_counts = self.permissions.count()
|
||||
node = TreeNode(**{
|
||||
'id': '$ROOT$',
|
||||
'name': f'所有权限({check_counts}/{total_counts})',
|
||||
'title': '所有权限',
|
||||
'pId': '',
|
||||
'chkDisabled': self.check_disabled,
|
||||
'isParent': True,
|
||||
'checked': total_counts == check_counts,
|
||||
'open': True,
|
||||
'meta': {
|
||||
'type': 'root',
|
||||
}
|
||||
})
|
||||
return node
|
||||
|
||||
def create_tree_nodes(self):
|
||||
nodes = [self._create_root_tree_node()]
|
||||
apps_nodes = self._create_apps_tree_nodes()
|
||||
models_nodes = self._create_models_tree_nodes()
|
||||
perms_nodes = self._create_perms_tree_nodes()
|
||||
|
||||
nodes += apps_nodes + models_nodes + perms_nodes
|
||||
return nodes
|
||||
|
||||
|
||||
class Permission(DjangoPermission):
|
||||
""" 权限类 """
|
||||
class Meta:
|
||||
|
@ -265,6 +77,7 @@ class Permission(DjangoPermission):
|
|||
|
||||
@staticmethod
|
||||
def create_tree_nodes(permissions, scope, check_disabled=False):
|
||||
from ..tree import PermissionTreeUtil
|
||||
util = PermissionTreeUtil(permissions, scope, check_disabled)
|
||||
return util.create_tree_nodes()
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ class RBACPermission(permissions.DjangoModelPermissions):
|
|||
('bulk_update', '%(app_label)s.change_%(model_name)s'),
|
||||
('partial_bulk_update', '%(app_label)s.change_%(model_name)s'),
|
||||
('bulk_destroy', '%(app_label)s.delete_%(model_name)s'),
|
||||
('render_to_json', '%(app_label)s.add_%(model_name)s'),
|
||||
('metadata', ''),
|
||||
('GET', '%(app_label)s.view_%(model_name)s'),
|
||||
('OPTIONS', ''),
|
||||
|
|
|
@ -0,0 +1,387 @@
|
|||
#!/usr/bin/python
|
||||
from collections import defaultdict
|
||||
from typing import Callable
|
||||
|
||||
from django.utils.translation import gettext_lazy as _, gettext
|
||||
from django.conf import settings
|
||||
from django.apps import apps
|
||||
from django.db.models import F, Count
|
||||
from django.utils.translation import ugettext
|
||||
|
||||
from .models import Permission, ContentType
|
||||
from common.tree import TreeNode
|
||||
|
||||
root_node_data = {
|
||||
'id': '$ROOT$',
|
||||
'name': _('All permissions'),
|
||||
'title': _('All permissions'),
|
||||
'pId': '',
|
||||
}
|
||||
|
||||
view_nodes_data = [
|
||||
{
|
||||
'id': 'view_console',
|
||||
'name': _('Console view'),
|
||||
},
|
||||
{
|
||||
'id': 'view_workspace',
|
||||
'name': _('Workspace view'),
|
||||
},
|
||||
{
|
||||
'id': 'view_audit',
|
||||
'name': _('Audit view'),
|
||||
},
|
||||
{
|
||||
'id': 'view_setting',
|
||||
'name': _('System setting'),
|
||||
},
|
||||
{
|
||||
'id': 'view_other',
|
||||
'name': _('Other'),
|
||||
}
|
||||
]
|
||||
|
||||
app_nodes_data = [
|
||||
{
|
||||
'id': 'users',
|
||||
'view': 'view_console',
|
||||
},
|
||||
{
|
||||
'id': 'assets',
|
||||
'view': 'view_console',
|
||||
},
|
||||
{
|
||||
'id': 'applications',
|
||||
'view': 'view_console',
|
||||
},
|
||||
{
|
||||
'id': 'accounts',
|
||||
'name': _('Accounts'),
|
||||
'view': 'view_console',
|
||||
},
|
||||
{
|
||||
'id': 'perms',
|
||||
'view': 'view_console',
|
||||
},
|
||||
{
|
||||
'id': 'acls',
|
||||
'view': 'view_console',
|
||||
},
|
||||
{
|
||||
'id': 'ops',
|
||||
'view': 'view_console',
|
||||
},
|
||||
{
|
||||
'id': 'terminal',
|
||||
'name': _('Session audits'),
|
||||
'view': 'view_audit',
|
||||
},
|
||||
{
|
||||
'id': 'audits',
|
||||
'view': 'view_audit',
|
||||
},
|
||||
{
|
||||
'id': 'rbac',
|
||||
'view': 'view_console'
|
||||
},
|
||||
{
|
||||
'id': 'settings',
|
||||
'view': 'view_setting'
|
||||
},
|
||||
{
|
||||
'id': 'tickets',
|
||||
'view': 'view_other',
|
||||
},
|
||||
{
|
||||
'id': 'authentication',
|
||||
'view': 'view_other'
|
||||
}
|
||||
]
|
||||
|
||||
extra_nodes_data = [
|
||||
{
|
||||
"id": "cloud_import",
|
||||
"name": _("Cloud import"),
|
||||
"pId": "assets",
|
||||
},
|
||||
{
|
||||
"id": "backup_account_node",
|
||||
"name": _("Backup account"),
|
||||
"pId": "accounts"
|
||||
},
|
||||
{
|
||||
"id": "gather_account_node",
|
||||
"name": _("Gather account"),
|
||||
"pId": "accounts",
|
||||
},
|
||||
{
|
||||
"id": "app_change_plan_node",
|
||||
"name": _("App change auth"),
|
||||
"pId": "accounts"
|
||||
},
|
||||
{
|
||||
"id": "asset_change_plan_node",
|
||||
"name": _("Asset change auth"),
|
||||
"pId": "accounts"
|
||||
},
|
||||
{
|
||||
"id": "terminal_node",
|
||||
"name": _("Terminal"),
|
||||
"pId": "view_setting"
|
||||
}
|
||||
]
|
||||
|
||||
special_pid_mapper = {
|
||||
'common.permission': 'view_other',
|
||||
"assets.authbook": "accounts",
|
||||
"applications.account": "accounts",
|
||||
'xpack.account': 'cloud_import',
|
||||
'xpack.syncinstancedetail': 'cloud_import',
|
||||
'xpack.syncinstancetask': 'cloud_import',
|
||||
'xpack.syncinstancetaskexecution': 'cloud_import',
|
||||
'assets.accountbackupplan': "backup_account_node",
|
||||
'assets.accountbackupplanexecution': "backup_account_node",
|
||||
'xpack.applicationchangeauthplan': 'app_change_plan_node',
|
||||
'xpack.applicationchangeauthplanexecution': 'app_change_plan_node',
|
||||
'xpack.applicationchangeauthplantask': 'app_change_plan_node',
|
||||
'xpack.changeauthplan': 'asset_change_plan_node',
|
||||
'xpack.changeauthplanexecution': 'asset_change_plan_node',
|
||||
'xpack.changeauthplantask': 'asset_change_plan_node',
|
||||
"assets.gathereduser": "gather_account_node",
|
||||
'xpack.gatherusertask': 'gather_account_node',
|
||||
'xpack.gatherusertaskexecution': 'gather_account_node',
|
||||
'orgs.organization': 'view_setting',
|
||||
'settings.setting': 'view_setting',
|
||||
'terminal.terminal': 'terminal_node',
|
||||
'terminal.commandstorage': 'terminal_node',
|
||||
'terminal.replaystorage': 'terminal_node',
|
||||
'terminal.status': 'terminal_node',
|
||||
'terminal.task': 'terminal_node',
|
||||
}
|
||||
|
||||
|
||||
class PermissionTreeUtil:
|
||||
get_permissions: Callable
|
||||
|
||||
def __init__(self, permissions, scope, check_disabled=False):
|
||||
self.permissions = self.prefetch_permissions(permissions)
|
||||
self.all_permissions = self.prefetch_permissions(
|
||||
Permission.get_permissions(scope)
|
||||
)
|
||||
self.check_disabled = check_disabled
|
||||
self.total_counts = defaultdict(int)
|
||||
self.checked_counts = defaultdict(int)
|
||||
|
||||
@staticmethod
|
||||
def prefetch_permissions(perms):
|
||||
return perms.select_related('content_type') \
|
||||
.annotate(app=F('content_type__app_label')) \
|
||||
.annotate(model=F('content_type__model'))
|
||||
|
||||
def create_apps_nodes(self):
|
||||
all_apps = apps.get_app_configs()
|
||||
apps_name_mapper = {
|
||||
app.name: app.verbose_name
|
||||
for app in all_apps if hasattr(app, 'verbose_name')
|
||||
}
|
||||
nodes = []
|
||||
|
||||
for i in app_nodes_data:
|
||||
app = i['id']
|
||||
name = i.get('name') or apps_name_mapper.get(app, app)
|
||||
view = i.get('view', 'other')
|
||||
|
||||
app_data = {
|
||||
'id': app,
|
||||
'name': name,
|
||||
'pId': view,
|
||||
}
|
||||
total_count = self.total_counts[app]
|
||||
checked_count = self.checked_counts[app]
|
||||
self.total_counts[view] += total_count
|
||||
self.checked_counts[view] += checked_count
|
||||
node = self._create_node(
|
||||
app_data, total_count, checked_count,
|
||||
'app', is_open=False
|
||||
)
|
||||
nodes.append(node)
|
||||
return nodes
|
||||
|
||||
def _get_model_counts_mapper(self):
|
||||
model_counts = self.all_permissions \
|
||||
.values('model', 'app', 'content_type') \
|
||||
.order_by('content_type') \
|
||||
.annotate(count=Count('content_type'))
|
||||
model_check_counts = self.permissions \
|
||||
.values('content_type', 'model') \
|
||||
.order_by('content_type') \
|
||||
.annotate(count=Count('content_type'))
|
||||
model_counts_mapper = {
|
||||
i['content_type']: i['count']
|
||||
for i in model_counts
|
||||
}
|
||||
model_check_counts_mapper = {
|
||||
i['content_type']: i['count']
|
||||
for i in model_check_counts
|
||||
}
|
||||
return model_counts_mapper, model_check_counts_mapper
|
||||
|
||||
def _create_models_nodes(self):
|
||||
content_types = ContentType.objects.all()
|
||||
total_counts_mapper, checked_counts_mapper = self._get_model_counts_mapper()
|
||||
|
||||
nodes = []
|
||||
for ct in content_types:
|
||||
total_count = total_counts_mapper.get(ct.id, 0)
|
||||
checked_count = checked_counts_mapper.get(ct.id, 0)
|
||||
if total_count == 0:
|
||||
continue
|
||||
|
||||
model_id = '{}.{}'.format(ct.app_label, ct.model)
|
||||
app = ct.app_label
|
||||
if special_pid_mapper.get(model_id):
|
||||
app = special_pid_mapper[model_id]
|
||||
|
||||
self.total_counts[app] += total_count
|
||||
self.checked_counts[app] += checked_count
|
||||
|
||||
name = f'{ct.name}'
|
||||
node = self._create_node({
|
||||
'id': model_id,
|
||||
'name': name,
|
||||
'pId': app,
|
||||
}, total_count, checked_count, 'model', is_open=False)
|
||||
nodes.append(node)
|
||||
return nodes
|
||||
|
||||
@staticmethod
|
||||
def _get_permission_name(p, content_types_name_mapper):
|
||||
code_name = p.codename
|
||||
action_mapper = {
|
||||
'add': ugettext('Create'),
|
||||
'view': ugettext('View'),
|
||||
'change': ugettext('Update'),
|
||||
'delete': ugettext('Delete')
|
||||
}
|
||||
name = ''
|
||||
ct = ''
|
||||
if 'add_' in p.codename:
|
||||
name = action_mapper['add']
|
||||
ct = code_name.replace('add_', '')
|
||||
elif 'view_' in p.codename:
|
||||
name = action_mapper['view']
|
||||
ct = code_name.replace('view_', '')
|
||||
elif 'change_' in p.codename:
|
||||
name = action_mapper['change']
|
||||
ct = code_name.replace('change_', '')
|
||||
elif 'delete' in code_name:
|
||||
name = action_mapper['delete']
|
||||
ct = code_name.replace('delete_', '')
|
||||
|
||||
if ct in content_types_name_mapper:
|
||||
name += content_types_name_mapper[ct]
|
||||
else:
|
||||
name = gettext(p.name)
|
||||
name = name.replace('Can ', '').replace('可以', '')
|
||||
return name
|
||||
|
||||
def _create_perms_nodes(self):
|
||||
permissions_id = self.permissions.values_list('id', flat=True)
|
||||
nodes = []
|
||||
content_types = ContentType.objects.all()
|
||||
content_types_name_mapper = {ct.model: ct.name for ct in content_types}
|
||||
|
||||
for p in self.all_permissions:
|
||||
model_id = f'{p.app}.{p.model}'
|
||||
name = self._get_permission_name(p, content_types_name_mapper)
|
||||
if settings.DEBUG:
|
||||
name += '({})'.format(p.app_label_codename)
|
||||
|
||||
node = TreeNode(**{
|
||||
'id': p.id,
|
||||
'name': name,
|
||||
'title': p.name,
|
||||
'pId': model_id,
|
||||
'isParent': False,
|
||||
'chkDisabled': self.check_disabled,
|
||||
'iconSkin': 'file',
|
||||
'checked': p.id in permissions_id,
|
||||
'open': False,
|
||||
'meta': {
|
||||
'type': 'perm',
|
||||
}
|
||||
})
|
||||
nodes.append(node)
|
||||
return nodes
|
||||
|
||||
def _create_node(self, data, total_count, checked_count, tp,
|
||||
is_parent=True, is_open=True, icon='', checked=None):
|
||||
assert data.get('id')
|
||||
assert data.get('name')
|
||||
assert data.get('pId') is not None
|
||||
if checked is None:
|
||||
checked = total_count == checked_count
|
||||
node_data = {
|
||||
'isParent': is_parent,
|
||||
'iconSkin': icon,
|
||||
'open': is_open,
|
||||
'chkDisabled': self.check_disabled,
|
||||
'checked': checked,
|
||||
'meta': {
|
||||
'type': tp,
|
||||
},
|
||||
**data
|
||||
}
|
||||
if not node_data.get('title'):
|
||||
node_data['title'] = node_data['name']
|
||||
node = TreeNode(**node_data)
|
||||
node.name += f'({checked_count}/{total_count})'
|
||||
return node
|
||||
|
||||
def _create_root_tree_node(self):
|
||||
total_count = self.all_permissions.count()
|
||||
checked_count = self.permissions.count()
|
||||
node = self._create_node(root_node_data, total_count, checked_count, 'root')
|
||||
return node
|
||||
|
||||
def _create_views_node(self):
|
||||
nodes = []
|
||||
for view_data in view_nodes_data:
|
||||
view = view_data['id']
|
||||
data = {
|
||||
**view_data,
|
||||
'pId': '$ROOT$',
|
||||
}
|
||||
total_count = self.total_counts[view]
|
||||
checked_count = self.checked_counts[view]
|
||||
node = self._create_node(data, total_count, checked_count, 'view')
|
||||
nodes.append(node)
|
||||
return nodes
|
||||
|
||||
def _create_extra_nodes(self):
|
||||
nodes = []
|
||||
for data in extra_nodes_data:
|
||||
i = data['id']
|
||||
pid = data['pId']
|
||||
checked_count = self.checked_counts[i]
|
||||
total_count = self.total_counts[i]
|
||||
self.total_counts[pid] += total_count
|
||||
self.checked_counts[pid] += checked_count
|
||||
node = self._create_node(
|
||||
data, total_count, checked_count,
|
||||
'extra', is_open=False
|
||||
)
|
||||
nodes.append(node)
|
||||
|
||||
return nodes
|
||||
|
||||
def create_tree_nodes(self):
|
||||
nodes = [self._create_root_tree_node()]
|
||||
perms_nodes = self._create_perms_nodes()
|
||||
models_nodes = self._create_models_nodes()
|
||||
apps_nodes = self.create_apps_nodes()
|
||||
views_nodes = self._create_views_node()
|
||||
extra_nodes = self._create_extra_nodes()
|
||||
|
||||
nodes += views_nodes + apps_nodes + models_nodes + perms_nodes + extra_nodes
|
||||
return nodes
|
|
@ -7,4 +7,4 @@ class SettingsConfig(AppConfig):
|
|||
verbose_name = _('Settings')
|
||||
|
||||
def ready(self):
|
||||
from . import signals_handler
|
||||
from . import signal_handlers
|
||||
|
|
|
@ -9,6 +9,6 @@ class TerminalConfig(AppConfig):
|
|||
verbose_name = _('Terminals')
|
||||
|
||||
def ready(self):
|
||||
from . import signals_handler
|
||||
from . import signal_handlers
|
||||
from . import notifications
|
||||
return super().ready()
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 3.1.14 on 2022-03-02 11:51
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('terminal', '0046_auto_20220228_1744'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='sessionreplay',
|
||||
options={'permissions': [('upload_sessionreplay', 'Can upload session replay'), ('download_sessionreplay', 'Can download session replay')], 'verbose_name': 'Session replay'},
|
||||
),
|
||||
]
|
|
@ -9,6 +9,7 @@ class SessionReplay(CommonModelMixin):
|
|||
session = models.ForeignKey(Session, on_delete=models.CASCADE, verbose_name=_("Session"))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Session replay")
|
||||
permissions = [
|
||||
('upload_sessionreplay', _("Can upload session replay")),
|
||||
('download_sessionreplay', _("Can download session replay")),
|
||||
|
|
|
@ -2,7 +2,7 @@ from rest_framework.generics import RetrieveDestroyAPIView
|
|||
|
||||
from orgs.utils import tmp_to_root_org
|
||||
from ..serializers import SuperTicketSerializer
|
||||
from ..models import SuperTicket
|
||||
from ..models import Ticket
|
||||
|
||||
|
||||
__all__ = ['SuperTicketStatusAPI']
|
||||
|
@ -10,11 +10,14 @@ __all__ = ['SuperTicketStatusAPI']
|
|||
|
||||
class SuperTicketStatusAPI(RetrieveDestroyAPIView):
|
||||
serializer_class = SuperTicketSerializer
|
||||
rbac_perms = {
|
||||
'GET': 'tickets.view_superticket',
|
||||
'DELETE': 'tickets.change_superticket'
|
||||
}
|
||||
|
||||
def get_queryset(self):
|
||||
with tmp_to_root_org():
|
||||
return SuperTicket.objects.all()
|
||||
return Ticket.objects.all()
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
ticket = self.get_object()
|
||||
ticket.close(processor=ticket.applicant)
|
||||
instance.close(processor=instance.applicant)
|
||||
|
|
|
@ -7,6 +7,6 @@ class TicketsConfig(AppConfig):
|
|||
verbose_name = _('Tickets')
|
||||
|
||||
def ready(self):
|
||||
from . import signals_handler
|
||||
from . import signal_handlers
|
||||
from . import notifications
|
||||
return super().ready()
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from typing import Callable
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from datetime import timedelta
|
||||
from django.db.utils import IntegrityError
|
||||
|
||||
from common.exceptions import JMSException
|
||||
|
@ -53,6 +54,7 @@ class StatusMixin:
|
|||
status: str
|
||||
applicant: models.ForeignKey
|
||||
current_node: models.Manager
|
||||
save: Callable
|
||||
|
||||
def set_state_approve(self):
|
||||
self.state = TicketState.approved
|
||||
|
@ -182,7 +184,8 @@ class Ticket(CommonModelMixin, StatusMixin, OrgModelMixin):
|
|||
|
||||
@property
|
||||
def processor(self):
|
||||
processor = self.current_node.first().ticket_assignees.exclude(state=ProcessStatus.notified).first()
|
||||
processor = self.current_node.first().ticket_assignees\
|
||||
.exclude(state=ProcessStatus.notified).first()
|
||||
return processor.assignee if processor else None
|
||||
|
||||
def create_related_node(self):
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework import serializers
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from ..models import SuperTicket
|
||||
|
||||
|
@ -6,7 +7,15 @@ from ..models import SuperTicket
|
|||
__all__ = ['SuperTicketSerializer']
|
||||
|
||||
|
||||
class SuperTicketSerializer(ModelSerializer):
|
||||
class SuperTicketSerializer(serializers.ModelSerializer):
|
||||
processor = serializers.SerializerMethodField(label=_("Processor"))
|
||||
|
||||
class Meta:
|
||||
model = SuperTicket
|
||||
fields = ['id', 'status', 'state', 'processor']
|
||||
|
||||
@staticmethod
|
||||
def get_processor(ticket):
|
||||
if not ticket.processor:
|
||||
return ''
|
||||
return str(ticket.processor)
|
||||
|
|
|
@ -9,6 +9,6 @@ class UsersConfig(AppConfig):
|
|||
verbose_name = _('Users')
|
||||
|
||||
def ready(self):
|
||||
from . import signals_handler
|
||||
from . import signal_handlers
|
||||
from . import notifications
|
||||
super().ready()
|
||||
|
|
|
@ -131,7 +131,7 @@ class UserSerializer(RolesSerializerMixin, CommonBulkSerializerMixin, serializer
|
|||
'public_key': {'write_only': True},
|
||||
'is_first_login': {'label': _('Is first login'), 'read_only': True},
|
||||
'is_valid': {'label': _('Is valid')},
|
||||
'is_service_account': {'label': _('Is service account')},
|
||||
'is_service_account': {'label': _('Is service account')},
|
||||
'is_expired': {'label': _('Is expired')},
|
||||
'avatar_url': {'label': _('Avatar url')},
|
||||
'created_by': {'read_only': True, 'allow_blank': True},
|
||||
|
@ -243,7 +243,7 @@ class InviteSerializer(RolesSerializerMixin, serializers.Serializer):
|
|||
class ServiceAccountSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['id', 'name', 'access_key']
|
||||
fields = ['id', 'name', 'access_key', 'comment']
|
||||
read_only_fields = ['access_key']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
|
10
jms
10
jms
|
@ -63,8 +63,8 @@ def check_database_connection():
|
|||
return
|
||||
except OperationalError:
|
||||
logging.info('Database not setup, retry')
|
||||
except Exception as e:
|
||||
logging.error('Unexpect error occur: {}'.format(str(e)))
|
||||
except Exception as exc:
|
||||
logging.error('Unexpect error occur: {}'.format(str(exc)))
|
||||
time.sleep(1)
|
||||
logging.error("Connection database failed, exit")
|
||||
sys.exit(10)
|
||||
|
@ -96,7 +96,7 @@ def collect_static():
|
|||
pass
|
||||
|
||||
|
||||
def compile_i81n_file():
|
||||
def compile_i18n_file():
|
||||
django_mo_file = os.path.join(BASE_DIR, 'apps', 'locale', 'zh', 'LC_MESSAGES', 'django.mo')
|
||||
if os.path.exists(django_mo_file):
|
||||
return
|
||||
|
@ -134,8 +134,8 @@ def start_services():
|
|||
except KeyboardInterrupt:
|
||||
logging.info('Cancel ...')
|
||||
time.sleep(2)
|
||||
except Exception as e:
|
||||
logging.error("Start service error {}: {}".format(services, e))
|
||||
except Exception as exc:
|
||||
logging.error("Start service error {}: {}".format(services, exc))
|
||||
time.sleep(2)
|
||||
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ decorator==4.1.2
|
|||
Django==3.1.14
|
||||
django-auth-ldap==2.2.0
|
||||
django-bootstrap3==14.2.0
|
||||
django-celery-beat==2.0
|
||||
django-celery-beat==2.2.1
|
||||
django-filter==2.4.0
|
||||
django-formtools==2.2
|
||||
django-ranged-response==0.2.0
|
||||
|
@ -84,7 +84,7 @@ python-daemon==2.2.3
|
|||
httpsig==1.3.0
|
||||
treelib==1.5.3
|
||||
django-proxy==1.2.1
|
||||
flower==0.9.3
|
||||
flower==1.0.0
|
||||
channels-redis==3.2.0
|
||||
channels==2.4.0
|
||||
daphne==2.4.1
|
||||
|
|
|
@ -22,8 +22,9 @@ redis = Redis(host=CONFIG.REDIS_HOST, port=CONFIG.REDIS_PORT, password=CONFIG.RE
|
|||
scheduler = "django_celery_beat.schedulers:DatabaseScheduler"
|
||||
|
||||
cmd = [
|
||||
'celery', 'beat',
|
||||
'celery',
|
||||
'-A', 'ops',
|
||||
'beat',
|
||||
'-l', 'INFO',
|
||||
'--scheduler', scheduler,
|
||||
'--max-interval', '60'
|
||||
|
|
Loading…
Reference in New Issue