* 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
fit2bot 2022-03-02 20:48:43 +08:00 committed by GitHub
parent 04e46e4b1c
commit dafc416783
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
69 changed files with 929 additions and 519 deletions

View File

@ -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

View File

@ -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

View File

@ -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')),
]

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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'},
),
]

View File

@ -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'},
),
]

View File

@ -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',),
),
]

View File

@ -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")

View File

@ -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()

View File

@ -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>

View File

@ -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

View File

@ -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):

View File

@ -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,

View File

@ -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',

View File

@ -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',

View File

@ -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:

View File

@ -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

View File

@ -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()

View File

@ -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__)

View File

@ -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()

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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'),
)

View File

@ -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()

View File

@ -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', ''),

387
apps/rbac/tree.py Normal file
View File

@ -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

View File

@ -7,4 +7,4 @@ class SettingsConfig(AppConfig):
verbose_name = _('Settings')
def ready(self):
from . import signals_handler
from . import signal_handlers

View File

@ -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()

View File

@ -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'},
),
]

View File

@ -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")),

View File

@ -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)

View File

@ -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()

View File

@ -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):

View File

@ -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)

View File

@ -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()

View File

@ -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
View File

@ -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)

View File

@ -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

View File

@ -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'