Compare commits

...

15 Commits

Author SHA1 Message Date
jiangweidong
dceef9c340 fix: 修复部署在无证书的Redis上,定时任务不执行的问题-v2.21 2022-06-28 12:34:16 +08:00
Jiangjie.Bai
5c174430df fix: 修复会话加入记录更新失败的问题 2022-06-16 16:51:10 +08:00
Jiangjie.Bai
8cbc8c0a34 fix: 修复命令列表模糊搜索报错500的问题
fix: 修复命令列表模糊搜索报错500的问题
2022-06-16 13:46:01 +08:00
Jiangjie.Bai
4520051d40 fix: 修复手动登录系统用户连接RemoteApp应用获取不到认证信息的问题 2022-06-16 10:41:59 +08:00
ibuler
b660d2c826 perf: 继续优化一波 2022-06-13 16:46:42 +08:00
ibuler
2085751cae perf: 优化迁移 rbac 速度
perf: migrate
2022-06-13 15:17:27 +08:00
feng626
76453c9a74 fix: 修复用户更新自己密码 url 不准确问题 2022-05-24 11:15:56 +08:00
ibuler
60af7a4e62 perf: 恢复 tickets 2022-05-17 18:00:37 +08:00
Jiangjie.Bai
622bef07ad fix: 修复获取类型为null的命令显示不支持的问题
fix: 修复获取类型为null的命令显示不支持的问题
2022-05-07 17:56:33 +08:00
fit2bot
a23bd4b3eb fix: 修复system-role获取users失败的问题 (#8197)
Co-authored-by: Jiangjie.Bai <bugatti_it@163.com>
2022-05-07 10:40:01 +08:00
Jiangjie.Bai
9bd9d443b4 fix: 修复windows执行ansible显示sudo失败的问题 2022-05-05 11:40:37 +08:00
feng626
e56fc93a6e fix: 组织管理员 添加 view platform perm 2022-04-28 19:10:15 +08:00
fit2bot
eb17183d97 fix: workbench_orgs 去重 (#8151)
Co-authored-by: feng626 <1304903146@qq.com>
2022-04-25 11:39:51 +08:00
feng626
b0057ecb9d perf: client download 2022-04-24 15:09:50 +08:00
ibuler
a141a8d2c2 fix: 修复社区版跳转问题 2022-04-21 22:48:34 +08:00
17 changed files with 186 additions and 84 deletions

View File

@@ -301,7 +301,7 @@ class Asset(AbsConnectivity, AbsHardwareInfo, ProtocolsMixin, NodesRelationMixin
'private_key': auth_user.private_key_file 'private_key': auth_user.private_key_file
} }
if not with_become: if not with_become or self.is_windows():
return info return info
if become_user: if become_user:

View File

@@ -133,6 +133,15 @@ class AuthMixin:
self.password = password self.password = password
def load_app_more_auth(self, app_id=None, username=None, user_id=None): def load_app_more_auth(self, app_id=None, username=None, user_id=None):
# 清除认证信息
self._clean_auth_info_if_manual_login_mode()
# 先加载临时认证信息
if self.login_mode == self.LOGIN_MANUAL:
self._load_tmp_auth_if_has(app_id, user_id)
return
# Remote app
from applications.models import Application from applications.models import Application
app = get_object_or_none(Application, pk=app_id) app = get_object_or_none(Application, pk=app_id)
if app and app.category_remote_app: if app and app.category_remote_app:
@@ -141,11 +150,6 @@ class AuthMixin:
return return
# Other app # Other app
self._clean_auth_info_if_manual_login_mode()
# 加载临时认证信息
if self.login_mode == self.LOGIN_MANUAL:
self._load_tmp_auth_if_has(app_id, user_id)
return
# 更新用户名 # 更新用户名
from users.models import User from users.models import User
user = get_object_or_none(User, pk=user_id) if user_id else None user = get_object_or_none(User, pk=user_id) if user_id else None

View File

@@ -387,6 +387,8 @@ class Config(dict):
'FTP_LOG_KEEP_DAYS': 200, 'FTP_LOG_KEEP_DAYS': 200,
'CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS': 30, 'CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS': 30,
'TICKETS_ENABLED': True,
# 废弃的 # 废弃的
'DEFAULT_ORG_SHOW_ALL_USERS': True, 'DEFAULT_ORG_SHOW_ALL_USERS': True,
'ORG_CHANGE_TO_URL': '', 'ORG_CHANGE_TO_URL': '',

View File

@@ -119,6 +119,7 @@ CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED = CONFIG.CHANGE_AUTH_PLAN_SECURE_MODE_ENABL
DATETIME_DISPLAY_FORMAT = '%Y-%m-%d %H:%M:%S' DATETIME_DISPLAY_FORMAT = '%Y-%m-%d %H:%M:%S'
TICKETS_ENABLED = CONFIG.TICKETS_ENABLED
REFERER_CHECK_ENABLED = CONFIG.REFERER_CHECK_ENABLED REFERER_CHECK_ENABLED = CONFIG.REFERER_CHECK_ENABLED
CONNECTION_TOKEN_ENABLED = CONFIG.CONNECTION_TOKEN_ENABLED CONNECTION_TOKEN_ENABLED = CONFIG.CONNECTION_TOKEN_ENABLED

View File

@@ -8,6 +8,7 @@ from ..filters import RoleFilter
from ..serializers import RoleSerializer, RoleUserSerializer from ..serializers import RoleSerializer, RoleUserSerializer
from ..models import Role, SystemRole, OrgRole from ..models import Role, SystemRole, OrgRole
from .permission import PermissionViewSet from .permission import PermissionViewSet
from common.mixins.api import PaginatedResponseMixin
__all__ = [ __all__ = [
'RoleViewSet', 'SystemRoleViewSet', 'OrgRoleViewSet', 'RoleViewSet', 'SystemRoleViewSet', 'OrgRoleViewSet',
@@ -15,7 +16,7 @@ __all__ = [
] ]
class RoleViewSet(JMSModelViewSet): class RoleViewSet(PaginatedResponseMixin, JMSModelViewSet):
queryset = Role.objects.all() queryset = Role.objects.all()
serializer_classes = { serializer_classes = {
'default': RoleSerializer, 'default': RoleSerializer,
@@ -54,7 +55,7 @@ class RoleViewSet(JMSModelViewSet):
def users(self, *args, **kwargs): def users(self, *args, **kwargs):
role = self.get_object() role = self.get_object()
queryset = role.users queryset = role.users
return self.get_paginated_response_with_query_set(queryset) return self.get_paginated_response_from_queryset(queryset)
class SystemRoleViewSet(RoleViewSet): class SystemRoleViewSet(RoleViewSet):

View File

@@ -126,6 +126,8 @@ class BuiltinRole:
org_user = PredefineRole( org_user = PredefineRole(
'7', ugettext_noop('OrgUser'), Scope.org, user_perms '7', ugettext_noop('OrgUser'), Scope.org, user_perms
) )
system_role_mapper = None
org_role_mapper = None
@classmethod @classmethod
def get_roles(cls): def get_roles(cls):
@@ -138,22 +140,24 @@ class BuiltinRole:
@classmethod @classmethod
def get_system_role_by_old_name(cls, name): def get_system_role_by_old_name(cls, name):
mapper = { if not cls.system_role_mapper:
'App': cls.system_component, cls.system_role_mapper = {
'Admin': cls.system_admin, 'App': cls.system_component.get_role(),
'User': cls.system_user, 'Admin': cls.system_admin.get_role(),
'Auditor': cls.system_auditor 'User': cls.system_user.get_role(),
'Auditor': cls.system_auditor.get_role()
} }
return mapper[name].get_role() return cls.system_role_mapper[name]
@classmethod @classmethod
def get_org_role_by_old_name(cls, name): def get_org_role_by_old_name(cls, name):
mapper = { if not cls.org_role_mapper:
'Admin': cls.org_admin, cls.org_role_mapper = {
'User': cls.org_user, 'Admin': cls.org_admin.get_role(),
'Auditor': cls.org_auditor, 'User': cls.org_user.get_role(),
'Auditor': cls.org_auditor.get_role(),
} }
return mapper[name].get_role() return cls.org_role_mapper[name]
@classmethod @classmethod
def sync_to_db(cls, show_msg=False): def sync_to_db(cls, show_msg=False):

View File

@@ -91,7 +91,7 @@ exclude_permissions = (
only_system_permissions = ( only_system_permissions = (
('assets', 'platform', '*', '*'), ('assets', 'platform', 'add,change,delete', 'platform'),
('users', 'user', 'delete', 'user'), ('users', 'user', 'delete', 'user'),
('rbac', 'role', 'delete,add,change', 'role'), ('rbac', 'role', 'delete,add,change', 'role'),
('rbac', 'systemrole', '*', '*'), ('rbac', 'systemrole', '*', '*'),

View File

@@ -1,5 +1,6 @@
# Generated by Django 3.1.13 on 2021-12-01 11:01 # Generated by Django 3.1.13 on 2021-12-01 11:01
import time
from django.db import migrations from django.db import migrations
from rbac.builtin import BuiltinRole from rbac.builtin import BuiltinRole
@@ -9,33 +10,61 @@ def migrate_system_role_binding(apps, schema_editor):
db_alias = schema_editor.connection.alias db_alias = schema_editor.connection.alias
user_model = apps.get_model('users', 'User') user_model = apps.get_model('users', 'User')
role_binding_model = apps.get_model('rbac', 'SystemRoleBinding') role_binding_model = apps.get_model('rbac', 'SystemRoleBinding')
users = user_model.objects.using(db_alias).all()
count = 0
bulk_size = 1000
while True:
users = user_model.objects.using(db_alias) \
.only('role', 'id') \
.all()[count:count+bulk_size]
if not users:
break
role_bindings = [] role_bindings = []
start = time.time()
for user in users: for user in users:
role = BuiltinRole.get_system_role_by_old_name(user.role) role = BuiltinRole.get_system_role_by_old_name(user.role)
role_binding = role_binding_model(scope='system', user_id=user.id, role_id=role.id) role_binding = role_binding_model(scope='system', user_id=user.id, role_id=role.id)
role_bindings.append(role_binding) role_bindings.append(role_binding)
role_binding_model.objects.bulk_create(role_bindings, ignore_conflicts=True) role_binding_model.objects.bulk_create(role_bindings, ignore_conflicts=True)
print("Create role binding: {}-{} using: {:.2f}s".format(
count, count + len(users), time.time()-start
))
count += len(users)
def migrate_org_role_binding(apps, schema_editor): def migrate_org_role_binding(apps, schema_editor):
db_alias = schema_editor.connection.alias db_alias = schema_editor.connection.alias
org_member_model = apps.get_model('orgs', 'OrganizationMember') org_member_model = apps.get_model('orgs', 'OrganizationMember')
role_binding_model = apps.get_model('rbac', 'RoleBinding') role_binding_model = apps.get_model('rbac', 'RoleBinding')
members = org_member_model.objects.using(db_alias).all()
count = 0
bulk_size = 1000
while True:
members = org_member_model.objects.using(db_alias)\
.only('role', 'user_id', 'org_id')\
.all()[count:count+bulk_size]
if not members:
break
role_bindings = [] role_bindings = []
start = time.time()
for member in members: for member in members:
role = BuiltinRole.get_org_role_by_old_name(member.role) role = BuiltinRole.get_org_role_by_old_name(member.role)
role_binding = role_binding_model( role_binding = role_binding_model(
scope='org', scope='org',
user_id=member.user.id, user_id=member.user_id,
role_id=role.id, role_id=role.id,
org_id=member.org.id org_id=member.org_id
) )
role_bindings.append(role_binding) role_bindings.append(role_binding)
role_binding_model.objects.bulk_create(role_bindings) role_binding_model.objects.bulk_create(role_bindings, ignore_conflicts=True)
print("Create role binding: {}-{} using: {:.2f}s".format(
count, count + len(members), time.time()-start
))
count += len(members)
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@@ -1,6 +1,7 @@
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.db import models from django.db import models
from django.db.models import Q from django.db.models import Q
from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from rest_framework.serializers import ValidationError from rest_framework.serializers import ValidationError
@@ -111,10 +112,13 @@ class RoleBinding(JMSModel):
system_bindings = [b for b in bindings if b.scope == Role.Scope.system.value] system_bindings = [b for b in bindings if b.scope == Role.Scope.system.value]
# 工作台仅限于自己加入的组织 # 工作台仅限于自己加入的组织
if perm == 'rbac.view_workbench': if perm == 'rbac.view_workbench':
all_orgs = user.orgs.all() all_orgs = user.orgs.all().distinct()
else: else:
all_orgs = Organization.objects.all() all_orgs = Organization.objects.all()
if not settings.XPACK_ENABLED:
all_orgs = all_orgs.filter(id=Organization.DEFAULT_ID)
# 有系统级别的绑定,就代表在所有组织有这个权限 # 有系统级别的绑定,就代表在所有组织有这个权限
if system_bindings: if system_bindings:
orgs = all_orgs orgs = all_orgs

View File

@@ -56,6 +56,7 @@ class PublicSettingApi(generics.RetrieveAPIView):
# Performance # Performance
"LOGIN_TITLE": self.get_login_title(), "LOGIN_TITLE": self.get_login_title(),
"LOGO_URLS": self.get_logo_urls(), "LOGO_URLS": self.get_logo_urls(),
"TICKETS_ENABLED": settings.TICKETS_ENABLED,
"HELP_DOCUMENT_URL": settings.HELP_DOCUMENT_URL, "HELP_DOCUMENT_URL": settings.HELP_DOCUMENT_URL,
"HELP_SUPPORT_URL": settings.HELP_SUPPORT_URL, "HELP_SUPPORT_URL": settings.HELP_SUPPORT_URL,
# Auth # Auth

View File

@@ -15,45 +15,33 @@ p {
</style> </style>
<div style="margin: 0 200px"> <div style="margin: 0 200px">
<div class="group"> <div class="group">
<h2>JumpServer {% trans 'Client' %}</h2> <h2>JumpServer {% trans 'Client' %} v1.1.4</h2>
<p> <p>
{% trans 'JumpServer Client, currently used to launch the client, now only support launch RDP SSH client, The Telnet client will next' %} {% trans 'JumpServer Client, currently used to launch the client, now only support launch RDP SSH client, The Telnet client will next' %}
{# //JumpServer 客户端,支持 RDP 的本地拉起,后续会支持拉起 ssh。#}
</p> </p>
<ul> <ul>
<li> <a href="/download/JumpServer-Client-Installer.msi">Windows {% trans 'Client' %}</a></li> <li> <a href="/download/JumpServer-Client-Installer-x86_64.msi">jumpserver-client-windows-x86_64.msi</a></li>
<li> <a href="/download/JumpServer-Client-Installer.dmg">macOS {% trans 'Client' %}</a></li> <li> <a href="/download/JumpServer-Client-Installer-arm64.msi">jumpserver-client-windows-arm64.msi</a></li>
<li> <a href="/download/JumpServer-Client-Installer.dmg">jumpserver-client-darwin.dmg</a></li>
</ul> </ul>
</div> </div>
<div class="group"> <div class="group">
<h2>{% trans 'Microsoft' %} RDP {% trans 'Official' %}{% trans 'Client' %}</h2> <h2>{% trans 'Microsoft' %} RDP {% trans 'Official' %}{% trans 'Client' %} v10.6.7</h2>
<p> <p>
{% trans 'macOS needs to download the client to connect RDP asset, which comes with Windows' %} {% trans 'macOS needs to download the client to connect RDP asset, which comes with Windows' %}
</p> </p>
<ul> <ul>
<li><a href="/download/Microsoft_Remote_Desktop_10.6.7_installer.pkg">Microsoft_Remote_Desktop_10.6.7_installer.pkg</a></li> <li><a href="/download/Microsoft_Remote_Desktop_10.6.7_installer.pkg">microsoft-remote-desktop-installer.pkg</a></li>
</ul>
</div>
<div class="group">
<h2>SSH {% trans 'Client' %}</h2>
<p>
{% trans 'Windows needs to download the client to connect SSH assets, and the MacOS system uses its own terminal' %}
</p>
<ul>
<li><a href="/download/putty/w64/putty.exe">64-bit x86: Putty.exe</a></li>
<li><a href="/download/putty/wa64/putty.exe">64-bit Arm: Putty.exe</a></li>
<li><a href="/download/putty/w32/putty.exe">32-bit x86: Putty.exe</a></li>
</ul> </ul>
</div> </div>
{% if XPACK_ENABLED %} {% if XPACK_ENABLED %}
<div class="group"> <div class="group">
<h2>{% trans 'Windows Remote application publisher tools' %}</h2> <h2>{% trans 'Windows Remote application publisher tools' %} v2.0</h2>
<p>{% trans 'Jmservisor is the program used to pull up remote applications in Windows Remote Application publisher' %}</p> <p>{% trans 'Jmservisor is the program used to pull up remote applications in Windows Remote Application publisher' %}</p>
<ul> <ul>
<li><a href="/download/Jmservisor.msi">Jmservisor</a></li> <li><a href="/download/Jmservisor.msi">jmservisor.msi</a></li>
</ul> </ul>
</div> </div>
{% endif %} {% endif %}

View File

@@ -43,6 +43,9 @@ class SessionJoinRecordsViewSet(OrgModelViewSet):
) )
filterset_fields = search_fields filterset_fields = search_fields
model = models.SessionJoinRecord model = models.SessionJoinRecord
rbac_perms = {
'finished': 'terminal.change_sessionjoinrecord'
}
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
try: try:

View File

@@ -266,6 +266,9 @@ class QuerySet(DJQuerySet):
self._command_store_config = command_store_config self._command_store_config = command_store_config
self._storage = CommandStore(command_store_config) self._storage = CommandStore(command_store_config)
# 命令列表模糊搜索时报错
super().__init__()
@lazyproperty @lazyproperty
def _grouped_method_calls(self): def _grouped_method_calls(self):
_method_calls = {k: list(v) for k, v in groupby(self._method_calls, lambda x: x[0])} _method_calls = {k: list(v) for k, v in groupby(self._method_calls, lambda x: x[0])}

View File

@@ -89,17 +89,21 @@ class CommandStorage(CommonStorageModelMixin, CommonModelMixin):
return Terminal.objects.filter(command_storage=self.name, is_deleted=False).exists() return Terminal.objects.filter(command_storage=self.name, is_deleted=False).exists()
def get_command_queryset(self): def get_command_queryset(self):
if self.type_server: if self.type_null:
qs = Command.objects.all()
else:
if self.type not in TYPE_ENGINE_MAPPING:
logger.error(f'Command storage `{self.type}` not support')
return Command.objects.none() return Command.objects.none()
if self.type_server:
return Command.objects.all()
if self.type in TYPE_ENGINE_MAPPING:
engine_mod = import_module(TYPE_ENGINE_MAPPING[self.type]) engine_mod = import_module(TYPE_ENGINE_MAPPING[self.type])
qs = engine_mod.QuerySet(self.config) qs = engine_mod.QuerySet(self.config)
qs.model = Command qs.model = Command
return qs return qs
logger.error(f'Command storage `{self.type}` not support')
return Command.objects.none()
def save(self, force_insert=False, force_update=False, using=None, def save(self, force_insert=False, force_update=False, using=None,
update_fields=None): update_fields=None):
super().save() super().save()

View File

@@ -143,7 +143,7 @@ class PasswordExpirationReminderMsg(UserMessage):
subject = _('Password is about expire') subject = _('Password is about expire')
date_password_expired_local = timezone.localtime(user.date_password_expired) date_password_expired_local = timezone.localtime(user.date_password_expired)
update_password_url = urljoin(settings.SITE_URL, '/ui/#/users/profile/?activeTab=PasswordUpdate') update_password_url = urljoin(settings.SITE_URL, '/ui/#/profile/setting/?activeTab=PasswordUpdate')
date_password_expired = date_password_expired_local.strftime('%Y-%m-%d %H:%M:%S') date_password_expired = date_password_expired_local.strftime('%Y-%m-%d %H:%M:%S')
context = { context = {
'name': user.name, 'name': user.name,

View File

@@ -12,33 +12,23 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
APPS_DIR = os.path.join(BASE_DIR, 'apps') APPS_DIR = os.path.join(BASE_DIR, 'apps')
sys.path.insert(0, BASE_DIR) sys.path.insert(0, BASE_DIR)
sys.path.insert(0, APPS_DIR)
from apps.jumpserver.const import CONFIG from apps.jumpserver.const import CONFIG
from apps.jumpserver.settings import base as jms_settings
os.environ.setdefault('PYTHONOPTIMIZE', '1') os.environ.setdefault('PYTHONOPTIMIZE', '1')
if os.getuid() == 0: if os.getuid() == 0:
os.environ.setdefault('C_FORCE_ROOT', '1') os.environ.setdefault('C_FORCE_ROOT', '1')
REDIS_SSL_KEYFILE = os.path.join(BASE_DIR, 'data', 'certs', 'redis_client.key')
if not os.path.exists(REDIS_SSL_KEYFILE):
REDIS_SSL_KEYFILE = None
REDIS_SSL_CERTFILE = os.path.join(BASE_DIR, 'data', 'certs', 'redis_client.crt')
if not os.path.exists(REDIS_SSL_CERTFILE):
REDIS_SSL_CERTFILE = None
REDIS_SSL_CA_CERTS = os.path.join(BASE_DIR, 'data', 'certs', 'redis_ca.crt')
if not os.path.exists(REDIS_SSL_CA_CERTS):
REDIS_SSL_CA_CERTS = os.path.join(BASE_DIR, 'data', 'certs', 'redis_ca.pem')
params = { params = {
'host': CONFIG.REDIS_HOST, 'host': CONFIG.REDIS_HOST,
'port': CONFIG.REDIS_PORT, 'port': CONFIG.REDIS_PORT,
'password': CONFIG.REDIS_PASSWORD, 'password': CONFIG.REDIS_PASSWORD,
"ssl": CONFIG.REDIS_USE_SSL, "ssl": CONFIG.REDIS_USE_SSL,
'ssl_cert_reqs': CONFIG.REDIS_SSL_REQUIRED, 'ssl_cert_reqs': CONFIG.REDIS_SSL_REQUIRED,
"ssl_keyfile": REDIS_SSL_KEYFILE, "ssl_keyfile": jms_settings.REDIS_SSL_KEYFILE,
"ssl_certfile": REDIS_SSL_CERTFILE, "ssl_certfile": jms_settings.REDIS_SSL_CERTFILE,
"ssl_ca_certs": REDIS_SSL_CA_CERTS "ssl_ca_certs": jms_settings.REDIS_SSL_CA_CERTS
} }
redis = Redis(**params) redis = Redis(**params)
scheduler = "django_celery_beat.schedulers:DatabaseScheduler" scheduler = "django_celery_beat.schedulers:DatabaseScheduler"

View File

@@ -0,0 +1,68 @@
# Generated by Django 3.1.13 on 2021-12-01 11:01
import os
import sys
import django
import time
app_path = '***** Change me *******'
sys.path.insert(0, app_path)
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "jumpserver.settings")
django.setup()
from django.apps import apps
from django.db import connection
# ========================== 添加到需要测试的 migrations 上方 ==========================
from django.db import migrations
from rbac.builtin import BuiltinRole
def migrate_system_role_binding(apps, schema_editor):
db_alias = schema_editor.connection.alias
user_model = apps.get_model('users', 'User')
role_binding_model = apps.get_model('rbac', 'SystemRoleBinding')
count = 0
bulk_size = 1000
while True:
users = user_model.objects.using(db_alias) \
.only('role', 'id') \
.all()[count:count+bulk_size]
if not users:
break
role_bindings = []
start = time.time()
for user in users:
role = BuiltinRole.get_system_role_by_old_name(user.role)
role_binding = role_binding_model(scope='system', user_id=user.id, role_id=role.id)
role_bindings.append(role_binding)
role_binding_model.objects.bulk_create(role_bindings, ignore_conflicts=True)
print("Create role binding: {}-{} using: {:.2f}s".format(
count, count + len(users), time.time()-start
))
count += len(users)
class Migration(migrations.Migration):
dependencies = [
('rbac', '0003_auto_20211130_1037'),
]
operations = [
migrations.RunPython(migrate_system_role_binding),
]
# ================== 添加到下方 ======================
def main():
schema_editor = connection.schema_editor()
migrate_system_role_binding(apps, schema_editor)
# main()