mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-12-15 08:32:48 +00:00
Compare commits
17 Commits
refactor_p
...
v2.23.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9cfbbe9157 | ||
|
|
ade78ef94e | ||
|
|
52fdac3853 | ||
|
|
70418ef8c8 | ||
|
|
58798094e5 | ||
|
|
a6367af7d3 | ||
|
|
dd2c736b8e | ||
|
|
bb5b390d1d | ||
|
|
16c01733f1 | ||
|
|
b1b63445db | ||
|
|
fca15eae7f | ||
|
|
2c63b56f62 | ||
|
|
ea5e56b33e | ||
|
|
e4819ffe11 | ||
|
|
c34302325f | ||
|
|
3b5ee06535 | ||
|
|
8e5edfd179 |
@@ -53,7 +53,15 @@ class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
|||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
def get_protocols(self, v):
|
def get_protocols(self, v):
|
||||||
return v.protocols.replace(' ', ', ')
|
""" protocols 是 queryset 中返回的,Post 创建成功后返回序列化时没有这个字段 """
|
||||||
|
if hasattr(v, 'protocols'):
|
||||||
|
protocols = v.protocols
|
||||||
|
elif hasattr(v, 'asset') and v.asset:
|
||||||
|
protocols = v.asset.protocols
|
||||||
|
else:
|
||||||
|
protocols = ''
|
||||||
|
protocols = protocols.replace(' ', ', ')
|
||||||
|
return protocols
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setup_eager_loading(cls, queryset):
|
def setup_eager_loading(cls, queryset):
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from .utils import validate_password_for_ansible
|
|||||||
|
|
||||||
class AuthSerializer(serializers.ModelSerializer):
|
class AuthSerializer(serializers.ModelSerializer):
|
||||||
password = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=1024, label=_('Password'))
|
password = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=1024, label=_('Password'))
|
||||||
private_key = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=4096, label=_('Private key'))
|
private_key = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=16384, label=_('Private key'))
|
||||||
|
|
||||||
def gen_keys(self, private_key=None, password=None):
|
def gen_keys(self, private_key=None, password=None):
|
||||||
if private_key is None:
|
if private_key is None:
|
||||||
@@ -38,7 +38,7 @@ class AuthSerializerMixin(serializers.ModelSerializer):
|
|||||||
validators=[validate_password_for_ansible]
|
validators=[validate_password_for_ansible]
|
||||||
)
|
)
|
||||||
private_key = EncryptedField(
|
private_key = EncryptedField(
|
||||||
label=_('SSH private key'), required=False, allow_blank=True, allow_null=True, max_length=4096
|
label=_('SSH private key'), required=False, allow_blank=True, allow_null=True, max_length=16384
|
||||||
)
|
)
|
||||||
passphrase = serializers.CharField(
|
passphrase = serializers.CharField(
|
||||||
allow_blank=True, allow_null=True, required=False, max_length=512,
|
allow_blank=True, allow_null=True, required=False, max_length=512,
|
||||||
|
|||||||
@@ -277,7 +277,6 @@ def on_user_auth_success(sender, user, request, login_type=None, **kwargs):
|
|||||||
check_different_city_login_if_need(user, request)
|
check_different_city_login_if_need(user, request)
|
||||||
data = generate_data(user.username, request, login_type=login_type)
|
data = generate_data(user.username, request, login_type=login_type)
|
||||||
request.session['login_time'] = data['datetime'].strftime("%Y-%m-%d %H:%M:%S")
|
request.session['login_time'] = data['datetime'].strftime("%Y-%m-%d %H:%M:%S")
|
||||||
request.session["MFA_VERIFY_TIME"] = int(time.time())
|
|
||||||
data.update({'mfa': int(user.mfa_enabled), 'status': True})
|
data.update({'mfa': int(user.mfa_enabled), 'status': True})
|
||||||
write_login_log(**data)
|
write_login_log(**data)
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ class SessionCookieMiddleware(MiddlewareMixin):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def set_cookie_public_key(request, response):
|
def set_cookie_public_key(request, response):
|
||||||
|
if request.path.startswith('/api'):
|
||||||
|
return
|
||||||
pub_key_name = settings.SESSION_RSA_PUBLIC_KEY_NAME
|
pub_key_name = settings.SESSION_RSA_PUBLIC_KEY_NAME
|
||||||
public_key = request.session.get(pub_key_name)
|
public_key = request.session.get(pub_key_name)
|
||||||
cookie_key = request.COOKIES.get(pub_key_name)
|
cookie_key = request.COOKIES.get(pub_key_name)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<!-- Stylesheets -->
|
<!-- Stylesheets -->
|
||||||
<link href="{% static 'css/login-style.css' %}" rel="stylesheet">
|
<link href="{% static 'css/login-style.css' %}" rel="stylesheet">
|
||||||
<link href="{% static 'css/jumpserver.css' %}" rel="stylesheet">
|
<link href="{% static 'css/jumpserver.css' %}" rel="stylesheet">
|
||||||
<script src="{% static "js/jumpserver.js" %}"></script>
|
<script src="{% static "js/jumpserver.js" %}?_=9"></script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.login-content {
|
.login-content {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<title>{{ title }}</title>
|
<title>{{ title }}</title>
|
||||||
{% include '_head_css_js.html' %}
|
{% include '_head_css_js.html' %}
|
||||||
<link href="{% static "css/jumpserver.css" %}" rel="stylesheet">
|
<link href="{% static "css/jumpserver.css" %}" rel="stylesheet">
|
||||||
<script src="{% static "js/jumpserver.js" %}"></script>
|
<script src="{% static "js/jumpserver.js" %}?_=9"></script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|||||||
@@ -325,6 +325,7 @@ class Config(dict):
|
|||||||
'TERMINAL_MAGNUS_ENABLED': True,
|
'TERMINAL_MAGNUS_ENABLED': True,
|
||||||
'TERMINAL_KOKO_SSH_ENABLED': True,
|
'TERMINAL_KOKO_SSH_ENABLED': True,
|
||||||
'TERMINAL_RAZOR_ENABLED': True,
|
'TERMINAL_RAZOR_ENABLED': True,
|
||||||
|
'TERMINAL_OMNIDB_ENABLED': True,
|
||||||
|
|
||||||
# 安全配置
|
# 安全配置
|
||||||
'SECURITY_MFA_AUTH': 0, # 0 不开启 1 全局开启 2 管理员开启
|
'SECURITY_MFA_AUTH': 0, # 0 不开启 1 全局开启 2 管理员开启
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ LOGIN_REDIRECT_MSG_ENABLED = CONFIG.LOGIN_REDIRECT_MSG_ENABLED
|
|||||||
CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS = CONFIG.CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS
|
CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS = CONFIG.CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS
|
||||||
|
|
||||||
TERMINAL_RAZOR_ENABLED = CONFIG.TERMINAL_RAZOR_ENABLED
|
TERMINAL_RAZOR_ENABLED = CONFIG.TERMINAL_RAZOR_ENABLED
|
||||||
|
TERMINAL_OMNIDB_ENABLED = CONFIG.TERMINAL_OMNIDB_ENABLED
|
||||||
TERMINAL_MAGNUS_ENABLED = CONFIG.TERMINAL_MAGNUS_ENABLED
|
TERMINAL_MAGNUS_ENABLED = CONFIG.TERMINAL_MAGNUS_ENABLED
|
||||||
TERMINAL_KOKO_SSH_ENABLED = CONFIG.TERMINAL_KOKO_SSH_ENABLED
|
TERMINAL_KOKO_SSH_ENABLED = CONFIG.TERMINAL_KOKO_SSH_ENABLED
|
||||||
|
|
||||||
|
|||||||
@@ -153,3 +153,5 @@ ANSIBLE_LOG_DIR = os.path.join(PROJECT_DIR, 'data', 'ansible')
|
|||||||
REDIS_HOST = CONFIG.REDIS_HOST
|
REDIS_HOST = CONFIG.REDIS_HOST
|
||||||
REDIS_PORT = CONFIG.REDIS_PORT
|
REDIS_PORT = CONFIG.REDIS_PORT
|
||||||
REDIS_PASSWORD = CONFIG.REDIS_PASSWORD
|
REDIS_PASSWORD = CONFIG.REDIS_PASSWORD
|
||||||
|
|
||||||
|
DJANGO_REDIS_SCAN_ITERSIZE = 1000
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ __all__ = [
|
|||||||
class CommonSettingSerializer(serializers.Serializer):
|
class CommonSettingSerializer(serializers.Serializer):
|
||||||
# OpenID 公有配置参数 (version <= 1.5.8 或 version >= 1.5.8)
|
# OpenID 公有配置参数 (version <= 1.5.8 或 version >= 1.5.8)
|
||||||
BASE_SITE_URL = serializers.CharField(
|
BASE_SITE_URL = serializers.CharField(
|
||||||
required=False, allow_null=True, max_length=1024, label=_('Base site url')
|
required=False, allow_null=True, allow_blank=True,
|
||||||
|
max_length=1024, label=_('Base site url')
|
||||||
)
|
)
|
||||||
AUTH_OPENID_CLIENT_ID = serializers.CharField(
|
AUTH_OPENID_CLIENT_ID = serializers.CharField(
|
||||||
required=False, max_length=1024, label=_('Client Id')
|
required=False, max_length=1024, label=_('Client Id')
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ class PrivateSettingSerializer(PublicSettingSerializer):
|
|||||||
TERMINAL_RAZOR_ENABLED = serializers.BooleanField()
|
TERMINAL_RAZOR_ENABLED = serializers.BooleanField()
|
||||||
TERMINAL_MAGNUS_ENABLED = serializers.BooleanField()
|
TERMINAL_MAGNUS_ENABLED = serializers.BooleanField()
|
||||||
TERMINAL_KOKO_SSH_ENABLED = serializers.BooleanField()
|
TERMINAL_KOKO_SSH_ENABLED = serializers.BooleanField()
|
||||||
|
TERMINAL_OMNIDB_ENABLED = serializers.BooleanField()
|
||||||
|
|
||||||
ANNOUNCEMENT_ENABLED = serializers.BooleanField()
|
ANNOUNCEMENT_ENABLED = serializers.BooleanField()
|
||||||
ANNOUNCEMENT = serializers.DictField()
|
ANNOUNCEMENT = serializers.DictField()
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
{% include '_head_css_js.html' %}
|
{% include '_head_css_js.html' %}
|
||||||
<link href="{% static "css/jumpserver.css" %}" rel="stylesheet">
|
<link href="{% static "css/jumpserver.css" %}" rel="stylesheet">
|
||||||
<script src="{% static "js/jumpserver.js" %}"></script>
|
<script src="{% static "js/jumpserver.js" %}?_=9"></script>
|
||||||
<style>
|
<style>
|
||||||
.outerBox {
|
.outerBox {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
{% include '_head_css_js.html' %}
|
{% include '_head_css_js.html' %}
|
||||||
<link href="{% static "css/jumpserver.css" %}" rel="stylesheet">
|
<link href="{% static "css/jumpserver.css" %}" rel="stylesheet">
|
||||||
<script src="{% static "js/jumpserver.js" %}"></script>
|
<script src="{% static "js/jumpserver.js" %}?_=9"></script>
|
||||||
<style>
|
<style>
|
||||||
.passwordBox {
|
.passwordBox {
|
||||||
max-width: 560px;
|
max-width: 560px;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<!-- Custom and plugin javascript -->
|
<!-- Custom and plugin javascript -->
|
||||||
<script src="{% static "js/plugins/toastr/toastr.min.js" %}"></script>
|
<script src="{% static "js/plugins/toastr/toastr.min.js" %}"></script>
|
||||||
<script src="{% static "js/inspinia.js" %}"></script>
|
<script src="{% static "js/inspinia.js" %}"></script>
|
||||||
<script src="{% static "js/jumpserver.js" %}?v=8"></script>
|
<script src="{% static "js/jumpserver.js" %}?v=9"></script>
|
||||||
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
|
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
|
||||||
<script src="{% static 'js/plugins/select2/i18n/zh-CN.js' %}"></script>
|
<script src="{% static 'js/plugins/select2/i18n/zh-CN.js' %}"></script>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from common.utils import pretty_string
|
||||||
from .models import AbstractSessionCommand
|
from .models import AbstractSessionCommand
|
||||||
|
|
||||||
__all__ = ['SessionCommandSerializer', 'InsecureCommandAlertSerializer']
|
__all__ = ['SessionCommandSerializer', 'InsecureCommandAlertSerializer']
|
||||||
@@ -32,7 +33,7 @@ class SessionCommandSerializer(SimpleSessionCommandSerializer):
|
|||||||
"""使用这个类作为基础Command Log Serializer类, 用来序列化"""
|
"""使用这个类作为基础Command Log Serializer类, 用来序列化"""
|
||||||
|
|
||||||
id = serializers.UUIDField(read_only=True)
|
id = serializers.UUIDField(read_only=True)
|
||||||
system_user = serializers.CharField(max_length=64, label=_("System user"))
|
system_user = serializers.CharField(label=_("System user")) # 限制 64 字符,不能直接迁移成 128 字符,命令表数据量会比较大
|
||||||
output = serializers.CharField(max_length=2048, allow_blank=True, label=_("Output"))
|
output = serializers.CharField(max_length=2048, allow_blank=True, label=_("Output"))
|
||||||
risk_level_display = serializers.SerializerMethodField(label=_('Risk level display'))
|
risk_level_display = serializers.SerializerMethodField(label=_('Risk level display'))
|
||||||
timestamp = serializers.IntegerField(label=_('Timestamp'))
|
timestamp = serializers.IntegerField(label=_('Timestamp'))
|
||||||
@@ -43,3 +44,8 @@ class SessionCommandSerializer(SimpleSessionCommandSerializer):
|
|||||||
def get_risk_level_display(obj):
|
def get_risk_level_display(obj):
|
||||||
risk_mapper = dict(AbstractSessionCommand.RISK_LEVEL_CHOICES)
|
risk_mapper = dict(AbstractSessionCommand.RISK_LEVEL_CHOICES)
|
||||||
return risk_mapper.get(obj.risk_level)
|
return risk_mapper.get(obj.risk_level)
|
||||||
|
|
||||||
|
def validate_system_user(self, value):
|
||||||
|
if len(value) > 64:
|
||||||
|
value = pretty_string(value, 64)
|
||||||
|
return value
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import copy
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
@@ -77,14 +78,15 @@ class CommandStorage(CommonStorageModelMixin, CommonModelMixin):
|
|||||||
def config(self):
|
def config(self):
|
||||||
config = self.meta
|
config = self.meta
|
||||||
config.update({'TYPE': self.type})
|
config.update({'TYPE': self.type})
|
||||||
return config
|
return copy.deepcopy(config)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def valid_config(self):
|
def valid_config(self):
|
||||||
config = self.config
|
config = self.config
|
||||||
if self.type_es and config.get('INDEX_BY_DATE'):
|
if self.type_es and config.get('INDEX_BY_DATE'):
|
||||||
engine_mod = import_module(TYPE_ENGINE_MAPPING[self.type])
|
engine_mod = import_module(TYPE_ENGINE_MAPPING[self.type])
|
||||||
store = engine_mod.CommandStore(config)
|
# 这里使用一个全新的 config, 防止修改当前的 config
|
||||||
|
store = engine_mod.CommandStore(self.config)
|
||||||
store._ensure_index_exists()
|
store._ensure_index_exists()
|
||||||
index_prefix = config.get('INDEX') or 'jumpserver'
|
index_prefix = config.get('INDEX') or 'jumpserver'
|
||||||
date = local_now_date_display()
|
date = local_now_date_display()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from rest_framework import serializers
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
|
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
|
||||||
from common.utils.random import random_string
|
from common.utils.random import random_string
|
||||||
|
from common.utils.common import pretty_string
|
||||||
from ..models import SessionSharing, SessionJoinRecord
|
from ..models import SessionSharing, SessionJoinRecord
|
||||||
|
|
||||||
__all__ = ['SessionSharingSerializer', 'SessionJoinRecordSerializer']
|
__all__ = ['SessionSharingSerializer', 'SessionJoinRecordSerializer']
|
||||||
@@ -24,7 +25,7 @@ class SessionSharingSerializer(OrgResourceModelSerializerMixin):
|
|||||||
session = validated_data.get('session')
|
session = validated_data.get('session')
|
||||||
if session:
|
if session:
|
||||||
validated_data['creator_id'] = session.user_id
|
validated_data['creator_id'] = session.user_id
|
||||||
validated_data['created_by'] = str(session.user)
|
validated_data['created_by'] = pretty_string(str(session.user), max_length=32)
|
||||||
validated_data['org_id'] = session.org_id
|
validated_data['org_id'] = session.org_id
|
||||||
return super().create(validated_data)
|
return super().create(validated_data)
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet):
|
|||||||
}
|
}
|
||||||
filterset_class = TicketFilter
|
filterset_class = TicketFilter
|
||||||
search_fields = [
|
search_fields = [
|
||||||
'title', 'action', 'type', 'status', 'applicant_display'
|
'title', 'type', 'status', 'applicant_display'
|
||||||
]
|
]
|
||||||
ordering_fields = (
|
ordering_fields = (
|
||||||
'title', 'applicant_display', 'status', 'state', 'action_display',
|
'title', 'applicant_display', 'status', 'state', 'action_display',
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ django-celery-beat==2.2.1
|
|||||||
django-filter==2.4.0
|
django-filter==2.4.0
|
||||||
django-formtools==2.2
|
django-formtools==2.2
|
||||||
django-ranged-response==0.2.0
|
django-ranged-response==0.2.0
|
||||||
django-redis-cache==2.1.1
|
|
||||||
django-rest-swagger==2.2.0
|
django-rest-swagger==2.2.0
|
||||||
django-simple-captcha==0.5.13
|
django-simple-captcha==0.5.13
|
||||||
django-timezone-field==4.1.0
|
django-timezone-field==4.1.0
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user