Merge pull request #6136 from jumpserver/dev

v2.10.0 rc3
pull/6160/head
Jiangjie.Bai 2021-05-18 19:16:25 +08:00 committed by GitHub
commit 0de9b29fa9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 251 additions and 164 deletions

View File

@ -39,14 +39,14 @@ class RemoteAppSerializer(serializers.Serializer):
@staticmethod @staticmethod
def get_asset_info(obj): def get_asset_info(obj):
asset_id = obj.get('asset') asset_id = obj.get('asset')
if not asset_id or is_uuid(asset_id): if not asset_id or not is_uuid(asset_id):
return {} return {}
try: try:
asset = Asset.objects.filter(id=str(asset_id)).values_list('id', 'hostname') asset = Asset.objects.get(id=str(asset_id))
except ObjectDoesNotExist as e: except ObjectDoesNotExist as e:
logger.error(e) logger.error(e)
return {} return {}
if not asset: if not asset:
return {} return {}
asset_info = {'id': str(asset[0]), 'hostname': asset[1]} asset_info = {'id': str(asset.id), 'hostname': asset.hostname}
return asset_info return asset_info

View File

@ -21,7 +21,7 @@ class DingTalkQRUnBindBase(APIView):
if not user.dingtalk_id: if not user.dingtalk_id:
raise errors.DingTalkNotBound raise errors.DingTalkNotBound
user.dingtalk_id = '' user.dingtalk_id = None
user.save() user.save()
return Response() return Response()

View File

@ -21,7 +21,7 @@ class WeComQRUnBindBase(APIView):
if not user.wecom_id: if not user.wecom_id:
raise errors.WeComNotBound raise errors.WeComNotBound
user.wecom_id = '' user.wecom_id = None
user.save() user.save()
return Response() return Response()

View File

@ -8,7 +8,7 @@ from django.core.cache import cache
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from six import text_type from six import text_type
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend from django.contrib.auth.backends import ModelBackend as DJModelBackend
from rest_framework import HTTP_HEADER_ENCODING from rest_framework import HTTP_HEADER_ENCODING
from rest_framework import authentication, exceptions from rest_framework import authentication, exceptions
from common.auth import signature from common.auth import signature
@ -25,6 +25,11 @@ def get_request_date_header(request):
return date return date
class ModelBackend(DJModelBackend):
def user_can_authenticate(self, user):
return user.is_valid
class AccessKeyAuthentication(authentication.BaseAuthentication): class AccessKeyAuthentication(authentication.BaseAuthentication):
"""App使用Access key进行签名认证, 目前签名算法比较简单, """App使用Access key进行签名认证, 目前签名算法比较简单,
app注册或者手动建立后,会生成 access_key_id access_key_secret, app注册或者手动建立后,会生成 access_key_id access_key_secret,

View File

@ -8,7 +8,9 @@ from django.views.generic import TemplateView
from django.views import View from django.views import View
from django.conf import settings from django.conf import settings
from django.http.request import HttpRequest from django.http.request import HttpRequest
from django.db.utils import IntegrityError
from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.exceptions import APIException
from users.views import UserVerifyPasswordView from users.views import UserVerifyPasswordView
from users.utils import is_auth_password_time_valid from users.utils import is_auth_password_time_valid
@ -29,6 +31,20 @@ DINGTALK_STATE_SESSION_KEY = '_dingtalk_state'
class DingTalkQRMixin(PermissionsMixin, View): class DingTalkQRMixin(PermissionsMixin, View):
def dispatch(self, request, *args, **kwargs):
try:
return super().dispatch(request, *args, **kwargs)
except APIException as e:
try:
msg = e.detail['errmsg']
except Exception:
msg = _('DingTalk Error, Please contact your system administrator')
return self.get_failed_reponse(
'/',
_('DingTalk Error'),
msg
)
def verify_state(self): def verify_state(self):
state = self.request.GET.get('state') state = self.request.GET.get('state')
session_state = self.request.session.get(DINGTALK_STATE_SESSION_KEY) session_state = self.request.session.get(DINGTALK_STATE_SESSION_KEY)
@ -130,8 +146,15 @@ class DingTalkQRBindCallbackView(DingTalkQRMixin, View):
response = self.get_failed_reponse(redirect_url, msg, msg) response = self.get_failed_reponse(redirect_url, msg, msg)
return response return response
try:
user.dingtalk_id = userid user.dingtalk_id = userid
user.save() user.save()
except IntegrityError as e:
if e.args[0] == 1062:
msg = _('The DingTalk is already bound to another user')
response = self.get_failed_reponse(redirect_url, msg, msg)
return response
raise e
msg = _('Binding DingTalk successfully') msg = _('Binding DingTalk successfully')
response = self.get_success_reponse(redirect_url, msg, msg) response = self.get_success_reponse(redirect_url, msg, msg)

View File

@ -8,7 +8,9 @@ from django.views.generic import TemplateView
from django.views import View from django.views import View
from django.conf import settings from django.conf import settings
from django.http.request import HttpRequest from django.http.request import HttpRequest
from django.db.utils import IntegrityError
from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.exceptions import APIException
from users.views import UserVerifyPasswordView from users.views import UserVerifyPasswordView
from users.utils import is_auth_password_time_valid from users.utils import is_auth_password_time_valid
@ -29,6 +31,20 @@ WECOM_STATE_SESSION_KEY = '_wecom_state'
class WeComQRMixin(PermissionsMixin, View): class WeComQRMixin(PermissionsMixin, View):
def dispatch(self, request, *args, **kwargs):
try:
return super().dispatch(request, *args, **kwargs)
except APIException as e:
try:
msg = e.detail['errmsg']
except Exception:
msg = _('WeCom Error, Please contact your system administrator')
return self.get_failed_reponse(
'/',
_('WeCom Error'),
msg
)
def verify_state(self): def verify_state(self):
state = self.request.GET.get('state') state = self.request.GET.get('state')
session_state = self.request.session.get(WECOM_STATE_SESSION_KEY) session_state = self.request.session.get(WECOM_STATE_SESSION_KEY)
@ -128,8 +144,15 @@ class WeComQRBindCallbackView(WeComQRMixin, View):
response = self.get_failed_reponse(redirect_url, msg, msg) response = self.get_failed_reponse(redirect_url, msg, msg)
return response return response
try:
user.wecom_id = wecom_userid user.wecom_id = wecom_userid
user.save() user.save()
except IntegrityError as e:
if e.args[0] == 1062:
msg = _('The WeCom is already bound to another user')
response = self.get_failed_reponse(redirect_url, msg, msg)
return response
raise e
msg = _('Binding WeCom successfully') msg = _('Binding WeCom successfully')
response = self.get_success_reponse(redirect_url, msg, msg) response = self.get_success_reponse(redirect_url, msg, msg)

View File

@ -57,6 +57,8 @@ class BaseFileParser(BaseParser):
@staticmethod @staticmethod
def _replace_chinese_quote(s): def _replace_chinese_quote(s):
if not isinstance(s, str):
return s
trans_table = str.maketrans({ trans_table = str.maketrans({
'': '"', '': '"',
'': '"', '': '"',

View File

@ -21,8 +21,3 @@ class ResponseDataKeyError(APIException):
class NetError(APIException): class NetError(APIException):
default_code = 'net_error' default_code = 'net_error'
default_detail = _('Network error, please contact system administrator') default_detail = _('Network error, please contact system administrator')
class AccessTokenError(APIException):
default_code = 'access_token_error'
default_detail = 'Access token error, check config'

View File

@ -22,7 +22,7 @@ class RequestMixin:
logger.error(f'Response 200 but errcode is not 0: ' logger.error(f'Response 200 but errcode is not 0: '
f'errcode={errcode} ' f'errcode={errcode} '
f'errmsg={errmsg} ') f'errmsg={errmsg} ')
raise exce.ErrCodeNot0(detail=str(data.raw_data)) raise exce.ErrCodeNot0(detail=data.raw_data)
def check_http_is_200(self, response): def check_http_is_200(self, response):
if response.status_code != 200: if response.status_code != 200:
@ -31,7 +31,7 @@ class RequestMixin:
f'status_code={response.status_code} ' f'status_code={response.status_code} '
f'url={response.url}' f'url={response.url}'
f'\ncontent={response.content}') f'\ncontent={response.content}')
raise exce.HTTPNot200 raise exce.HTTPNot200(detail=response.json())
class BaseRequest(RequestMixin): class BaseRequest(RequestMixin):

View File

@ -39,7 +39,7 @@ class DictWrapper:
except KeyError as e: except KeyError as e:
msg = f'Response 200 but get field from json error: error={e} data={self.raw_data}' msg = f'Response 200 but get field from json error: error={e} data={self.raw_data}'
logger.error(msg) logger.error(msg)
raise exce.ResponseDataKeyError(detail=msg) raise exce.ResponseDataKeyError(detail=self.raw_data)
def __getattr__(self, item): def __getattr__(self, item):
return getattr(self.raw_data, item) return getattr(self.raw_data, item)

View File

@ -33,6 +33,9 @@ class ErrorCode:
# https://open.work.weixin.qq.com/api/doc/90000/90139/90313#%E9%94%99%E8%AF%AF%E7%A0%81%EF%BC%9A81013 # https://open.work.weixin.qq.com/api/doc/90000/90139/90313#%E9%94%99%E8%AF%AF%E7%A0%81%EF%BC%9A81013
RECIPIENTS_INVALID = 81013 # UserID、部门ID、标签ID全部非法或无权限。 RECIPIENTS_INVALID = 81013 # UserID、部门ID、标签ID全部非法或无权限。
# https: // open.work.weixin.qq.com / devtool / query?e = 82001
RECIPIENTS_EMPTY = 82001 # 指定的成员/部门/标签全部为空
# https://open.work.weixin.qq.com/api/doc/90000/90135/91437 # https://open.work.weixin.qq.com/api/doc/90000/90135/91437
INVALID_CODE = 40029 INVALID_CODE = 40029
@ -141,7 +144,7 @@ class WeCom(RequestMixin):
data = self._requests.post(URL.SEND_MESSAGE, json=body, check_errcode_is_0=False) data = self._requests.post(URL.SEND_MESSAGE, json=body, check_errcode_is_0=False)
errcode = data['errcode'] errcode = data['errcode']
if errcode == ErrorCode.RECIPIENTS_INVALID: if errcode in (ErrorCode.RECIPIENTS_INVALID, ErrorCode.RECIPIENTS_EMPTY):
# 全部接收人无权限或不存在 # 全部接收人无权限或不存在
return users return users
self.check_errcode_is_0(data) self.check_errcode_is_0(data)

View File

@ -120,7 +120,7 @@ LOGIN_CONFIRM_ENABLE = CONFIG.LOGIN_CONFIRM_ENABLE
OTP_IN_RADIUS = CONFIG.OTP_IN_RADIUS OTP_IN_RADIUS = CONFIG.OTP_IN_RADIUS
AUTH_BACKEND_MODEL = 'django.contrib.auth.backends.ModelBackend' AUTH_BACKEND_MODEL = 'authentication.backends.api.ModelBackend'
AUTH_BACKEND_PUBKEY = 'authentication.backends.pubkey.PublicKeyAuthBackend' AUTH_BACKEND_PUBKEY = 'authentication.backends.pubkey.PublicKeyAuthBackend'
AUTH_BACKEND_LDAP = 'authentication.backends.ldap.LDAPAuthorizationBackend' AUTH_BACKEND_LDAP = 'authentication.backends.ldap.LDAPAuthorizationBackend'
AUTH_BACKEND_OIDC_PASSWORD = 'jms_oidc_rp.backends.OIDCAuthPasswordBackend' AUTH_BACKEND_OIDC_PASSWORD = 'jms_oidc_rp.backends.OIDCAuthPasswordBackend'

View File

@ -16,7 +16,7 @@ PROJECT_DIR = const.PROJECT_DIR
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = CONFIG.SECRET_KEY SECRET_KEY = CONFIG.SECRET_KEY
# SECURITY WARNING: keep the token secret, remove it if all coco, guacamole ok # SECURITY WARNING: keep the token secret, remove it if all koko, lion ok
BOOTSTRAP_TOKEN = CONFIG.BOOTSTRAP_TOKEN BOOTSTRAP_TOKEN = CONFIG.BOOTSTRAP_TOKEN
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!

Binary file not shown.

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: JumpServer 0.3.3\n" "Project-Id-Version: JumpServer 0.3.3\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-05-14 16:12+0800\n" "POT-Creation-Date: 2021-05-17 18:56+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: ibuler <ibuler@qq.com>\n" "Last-Translator: ibuler <ibuler@qq.com>\n"
"Language-Team: JumpServer team<ibuler@qq.com>\n" "Language-Team: JumpServer team<ibuler@qq.com>\n"
@ -1547,44 +1547,48 @@ msgstr "返回"
msgid "Copy success" msgid "Copy success"
msgstr "复制成功" msgstr "复制成功"
#: authentication/views/dingtalk.py:40 authentication/views/wecom.py:40 #: authentication/views/dingtalk.py:41 authentication/views/wecom.py:41
msgid "You've been hacked" msgid "You've been hacked"
msgstr "你被攻击了" msgstr "你被攻击了"
#: authentication/views/dingtalk.py:76 #: authentication/views/dingtalk.py:77
msgid "DingTalk is already bound" msgid "DingTalk is already bound"
msgstr "钉钉已经绑定" msgstr "钉钉已经绑定"
#: authentication/views/dingtalk.py:89 authentication/views/wecom.py:88 #: authentication/views/dingtalk.py:90 authentication/views/wecom.py:89
msgid "Please verify your password first" msgid "Please verify your password first"
msgstr "请检查密码" msgstr "请检查密码"
#: authentication/views/dingtalk.py:113 authentication/views/wecom.py:112 #: authentication/views/dingtalk.py:114 authentication/views/wecom.py:113
msgid "Invalid user_id" msgid "Invalid user_id"
msgstr "无效的 user_id" msgstr "无效的 user_id"
#: authentication/views/dingtalk.py:129 #: authentication/views/dingtalk.py:130
msgid "DingTalk query user failed" msgid "DingTalk query user failed"
msgstr "钉钉查询用户失败" msgstr "钉钉查询用户失败"
#: authentication/views/dingtalk.py:136 authentication/views/dingtalk.py:219 #: authentication/views/dingtalk.py:139
#: authentication/views/dingtalk.py:220 msgid "The DingTalk is already bound to another user"
msgstr "该钉钉已经绑定其他用户"
#: authentication/views/dingtalk.py:144 authentication/views/dingtalk.py:227
#: authentication/views/dingtalk.py:228
msgid "Binding DingTalk successfully" msgid "Binding DingTalk successfully"
msgstr "绑定 钉钉 成功" msgstr "绑定 钉钉 成功"
#: authentication/views/dingtalk.py:188 #: authentication/views/dingtalk.py:196
msgid "Failed to get user from DingTalk" msgid "Failed to get user from DingTalk"
msgstr "从钉钉获取用户失败" msgstr "从钉钉获取用户失败"
#: authentication/views/dingtalk.py:194 #: authentication/views/dingtalk.py:202
msgid "DingTalk is not bound" msgid "DingTalk is not bound"
msgstr "钉钉没有绑定" msgstr "钉钉没有绑定"
#: authentication/views/dingtalk.py:195 authentication/views/wecom.py:193 #: authentication/views/dingtalk.py:203 authentication/views/wecom.py:201
msgid "Please login with a password and then bind the WeCom" msgid "Please login with a password and then bind the WeCom"
msgstr "请使用密码登录,然后绑定企业微信" msgstr "请使用密码登录,然后绑定企业微信"
#: authentication/views/dingtalk.py:237 authentication/views/dingtalk.py:238 #: authentication/views/dingtalk.py:245 authentication/views/dingtalk.py:246
msgid "Binding DingTalk failed" msgid "Binding DingTalk failed"
msgstr "绑定钉钉失败" msgstr "绑定钉钉失败"
@ -1620,28 +1624,32 @@ msgstr "退出登录成功"
msgid "Logout success, return login page" msgid "Logout success, return login page"
msgstr "退出登录成功,返回到登录页面" msgstr "退出登录成功,返回到登录页面"
#: authentication/views/wecom.py:75 #: authentication/views/wecom.py:76
msgid "WeCom is already bound" msgid "WeCom is already bound"
msgstr "企业微信已经绑定" msgstr "企业微信已经绑定"
#: authentication/views/wecom.py:127 #: authentication/views/wecom.py:128
msgid "WeCom query user failed" msgid "WeCom query user failed"
msgstr "企业微信查询用户失败" msgstr "企业微信查询用户失败"
#: authentication/views/wecom.py:134 authentication/views/wecom.py:217 #: authentication/views/wecom.py:137
#: authentication/views/wecom.py:218 msgid "The WeCom is already bound to another user"
msgstr "该企业微信已经绑定其他用户"
#: authentication/views/wecom.py:142 authentication/views/wecom.py:225
#: authentication/views/wecom.py:226
msgid "Binding WeCom successfully" msgid "Binding WeCom successfully"
msgstr "绑定 企业微信 成功" msgstr "绑定 企业微信 成功"
#: authentication/views/wecom.py:186 #: authentication/views/wecom.py:194
msgid "Failed to get user from WeCom" msgid "Failed to get user from WeCom"
msgstr "从企业微信获取用户失败" msgstr "从企业微信获取用户失败"
#: authentication/views/wecom.py:192 #: authentication/views/wecom.py:200
msgid "WeCom is not bound" msgid "WeCom is not bound"
msgstr "没有绑定企业微信" msgstr "没有绑定企业微信"
#: authentication/views/wecom.py:235 authentication/views/wecom.py:236 #: authentication/views/wecom.py:243 authentication/views/wecom.py:244
msgid "Binding WeCom failed" msgid "Binding WeCom failed"
msgstr "绑定企业微信失败" msgstr "绑定企业微信失败"
@ -1803,36 +1811,36 @@ msgstr "没有该主机 {} 权限"
msgid "Operations" msgid "Operations"
msgstr "运维" msgstr "运维"
#: ops/mixin.py:29 ops/mixin.py:92 ops/mixin.py:160 #: ops/mixin.py:29 ops/mixin.py:92 ops/mixin.py:162
msgid "Cycle perform" msgid "Cycle perform"
msgstr "周期执行" msgstr "周期执行"
#: ops/mixin.py:33 ops/mixin.py:90 ops/mixin.py:109 ops/mixin.py:148 #: ops/mixin.py:33 ops/mixin.py:90 ops/mixin.py:109 ops/mixin.py:150
msgid "Regularly perform" msgid "Regularly perform"
msgstr "定期执行" msgstr "定期执行"
#: ops/mixin.py:106 ops/mixin.py:145 #: ops/mixin.py:106 ops/mixin.py:147
#: xpack/plugins/change_auth_plan/serializers.py:53 #: xpack/plugins/change_auth_plan/serializers.py:53
msgid "Periodic perform" msgid "Periodic perform"
msgstr "定时执行" msgstr "定时执行"
#: ops/mixin.py:111 #: ops/mixin.py:112
msgid "Interval" msgid "Interval"
msgstr "间隔" msgstr "间隔"
#: ops/mixin.py:120 #: ops/mixin.py:122
msgid "* Please enter a valid crontab expression" msgid "* Please enter a valid crontab expression"
msgstr "* 请输入有效的 crontab 表达式" msgstr "* 请输入有效的 crontab 表达式"
#: ops/mixin.py:127 #: ops/mixin.py:129
msgid "Range {} to {}" msgid "Range {} to {}"
msgstr "输入在 {} - {} 范围之间" msgstr "输入在 {} - {} 范围之间"
#: ops/mixin.py:138 #: ops/mixin.py:140
msgid "Require periodic or regularly perform setting" msgid "Require periodic or regularly perform setting"
msgstr "需要周期或定期设置" msgstr "需要周期或定期设置"
#: ops/mixin.py:149 #: ops/mixin.py:151
msgid "" msgid ""
"eg: Every Sunday 03:05 run <5 3 * * 0> <br> Tips: Using 5 digits linux " "eg: Every Sunday 03:05 run <5 3 * * 0> <br> Tips: Using 5 digits linux "
"crontab expressions <min hour day month week> (<a href='https://tool.lu/" "crontab expressions <min hour day month week> (<a href='https://tool.lu/"
@ -1843,7 +1851,7 @@ msgstr ""
"分 时 日 月 星期> <a href='https://tool.lu/crontab/' target='_blank'>在线工" "分 时 日 月 星期> <a href='https://tool.lu/crontab/' target='_blank'>在线工"
"具</a> <br>注意: 如果同时设置了定期执行和周期执行,优先使用定期执行" "具</a> <br>注意: 如果同时设置了定期执行和周期执行,优先使用定期执行"
#: ops/mixin.py:160 #: ops/mixin.py:162
msgid "Tips: (Units: hour)" msgid "Tips: (Units: hour)"
msgstr "提示:(单位: 时)" msgstr "提示:(单位: 时)"
@ -1954,12 +1962,11 @@ msgstr "更新任务内容: {}"
msgid "Disk used more than 80%: {} => {}" msgid "Disk used more than 80%: {} => {}"
msgstr "磁盘使用率超过 80%: {} => {}" msgstr "磁盘使用率超过 80%: {} => {}"
#: orgs/api.py:76 #: orgs/api.py:79
#, python-brace-format msgid "Have {} exists, Please delete"
msgid "Have `{model._meta.verbose_name}` exists, Please delete" msgstr "{} 存在数据, 请先删除"
msgstr "`{model._meta.verbose_name}` 存在数据, 请先删除"
#: orgs/api.py:80 #: orgs/api.py:83
msgid "The current organization cannot be deleted" msgid "The current organization cannot be deleted"
msgstr "当前组织不能被删除" msgstr "当前组织不能被删除"
@ -2123,7 +2130,11 @@ msgstr "邮件已经发送{}, 请检查"
msgid "Welcome to the JumpServer open source Bastion Host" msgid "Welcome to the JumpServer open source Bastion Host"
msgstr "欢迎使用JumpServer开源堡垒机" msgstr "欢迎使用JumpServer开源堡垒机"
#: settings/api/dingtalk.py:36 settings/api/wecom.py:36 #: settings/api/dingtalk.py:29
msgid "AppSecret is required"
msgstr "AppSecret 是必须的"
#: settings/api/dingtalk.py:35 settings/api/wecom.py:35
msgid "OK" msgid "OK"
msgstr "" msgstr ""
@ -2135,6 +2146,10 @@ msgstr "获取 LDAP 用户为 None"
msgid "Imported {} users successfully" msgid "Imported {} users successfully"
msgstr "导入 {} 个用户成功" msgstr "导入 {} 个用户成功"
#: settings/api/wecom.py:29
msgid "Secret is required"
msgstr "Secret 是必须的"
#: settings/models.py:123 users/templates/users/reset_password.html:29 #: settings/models.py:123 users/templates/users/reset_password.html:29
msgid "Setting" msgid "Setting"
msgstr "设置" msgstr "设置"
@ -2419,16 +2434,13 @@ msgstr ""
#: settings/serializers/settings.py:172 #: settings/serializers/settings.py:172
msgid "Number of repeated historical passwords" msgid "Number of repeated historical passwords"
msgstr "历史密码可重复次数" msgstr "不能设置近几次密码"
#: settings/serializers/settings.py:173 #: settings/serializers/settings.py:173
msgid "" msgid ""
"Tip: When the user resets the password, it cannot be the previous n " "Tip: When the user resets the password, it cannot be the previous n "
"historical passwords of the user (the value of n here is the value filled in " "historical passwords of the user"
"the input box)" msgstr "提示:用户重置密码时,不能为该用户前几次使用过的密码"
msgstr ""
"提示用户重置密码时不能为该用户前n次历史密码 (此处的n值即为输入框中填写的"
"值)"
#: settings/serializers/settings.py:177 #: settings/serializers/settings.py:177
msgid "Password minimum length" msgid "Password minimum length"
@ -2462,34 +2474,10 @@ msgstr "邮件收件人"
msgid "Multiple user using , split" msgid "Multiple user using , split"
msgstr "多个用户,使用 , 分割" msgstr "多个用户,使用 , 分割"
#: settings/serializers/settings.py:193
msgid "Corporation ID(corpid)"
msgstr "企业 ID(CorpId)"
#: settings/serializers/settings.py:194
msgid "Agent ID(agentid)"
msgstr "应用 ID(AgentId)"
#: settings/serializers/settings.py:195
msgid "Secret(secret)"
msgstr "秘钥(secret)"
#: settings/serializers/settings.py:196 #: settings/serializers/settings.py:196
msgid "Enable WeCom Auth" msgid "Enable WeCom Auth"
msgstr "启用企业微信认证" msgstr "启用企业微信认证"
#: settings/serializers/settings.py:200
msgid "AgentId"
msgstr "应用 ID(AgentId)"
#: settings/serializers/settings.py:201
msgid "AppKey"
msgstr "应用 Key(AppKey)"
#: settings/serializers/settings.py:202
msgid "AppSecret"
msgstr "应用密文(AppSecret)"
#: settings/serializers/settings.py:203 #: settings/serializers/settings.py:203
msgid "Enable DingTalk Auth" msgid "Enable DingTalk Auth"
msgstr "启用钉钉认证" msgstr "启用钉钉认证"
@ -2896,7 +2884,7 @@ msgstr "取消"
#: templates/flash_message_standalone.html:37 #: templates/flash_message_standalone.html:37
msgid "Go" msgid "Go"
msgstr "" msgstr "立即"
#: templates/index.html:11 #: templates/index.html:11
msgid "Total users" msgid "Total users"
@ -3048,19 +3036,19 @@ msgstr "登录了"
msgid "Filters" msgid "Filters"
msgstr "过滤" msgstr "过滤"
#: terminal/api/session.py:189 #: terminal/api/session.py:185
msgid "Session does not exist: {}" msgid "Session does not exist: {}"
msgstr "会话不存在: {}" msgstr "会话不存在: {}"
#: terminal/api/session.py:192 #: terminal/api/session.py:188
msgid "Session is finished or the protocol not supported" msgid "Session is finished or the protocol not supported"
msgstr "会话已经完成或协议不支持" msgstr "会话已经完成或协议不支持"
#: terminal/api/session.py:197 #: terminal/api/session.py:193
msgid "User does not exist: {}" msgid "User does not exist: {}"
msgstr "用户不存在: {}" msgstr "用户不存在: {}"
#: terminal/api/session.py:201 #: terminal/api/session.py:197
msgid "User does not have permission" msgid "User does not have permission"
msgstr "用户没有权限" msgstr "用户没有权限"
@ -3094,7 +3082,7 @@ msgstr "测试失败: 账户无效"
#: terminal/backends/command/es.py:27 #: terminal/backends/command/es.py:27
msgid "Invalid elasticsearch config" msgid "Invalid elasticsearch config"
msgstr "" msgstr "无效的 Elasticsearch 配置"
#: terminal/backends/command/models.py:14 #: terminal/backends/command/models.py:14
msgid "Ordinary" msgid "Ordinary"
@ -4953,7 +4941,7 @@ msgstr "实例个数"
msgid "Periodic display" msgid "Periodic display"
msgstr "定时执行" msgstr "定时执行"
#: xpack/plugins/cloud/utils.py:65 #: xpack/plugins/cloud/utils.py:64
msgid "Account unavailable" msgid "Account unavailable"
msgstr "账户无效" msgstr "账户无效"
@ -5041,5 +5029,23 @@ msgstr "旗舰版"
msgid "Community edition" msgid "Community edition"
msgstr "社区版" msgstr "社区版"
#~ msgid "Corporation ID(corpid)"
#~ msgstr "企业 ID(CorpId)"
#~ msgid "Agent ID(agentid)"
#~ msgstr "应用 ID(AgentId)"
#~ msgid "Secret(secret)"
#~ msgstr "秘钥(secret)"
#~ msgid "AgentId"
#~ msgstr "应用 ID(AgentId)"
#~ msgid "AppKey"
#~ msgstr "应用 Key(AppKey)"
#~ msgid "AppSecret"
#~ msgstr "应用密文(AppSecret)"
#~ msgid "No" #~ msgid "No"
#~ msgstr "无" #~ msgstr "无"

View File

@ -103,12 +103,14 @@ class PeriodTaskModelMixin(models.Model):
class PeriodTaskSerializerMixin(serializers.Serializer): class PeriodTaskSerializerMixin(serializers.Serializer):
is_periodic = serializers.BooleanField(default=False, label=_("Periodic perform")) is_periodic = serializers.BooleanField(default=True, label=_("Periodic perform"))
crontab = serializers.CharField( crontab = serializers.CharField(
max_length=128, allow_blank=True, max_length=128, allow_blank=True,
allow_null=True, required=False, label=_('Regularly perform') allow_null=True, required=False, label=_('Regularly perform')
) )
interval = serializers.IntegerField(allow_null=True, required=False, label=_('Interval')) interval = serializers.IntegerField(
default=24, allow_null=True, required=False, label=_('Interval')
)
INTERVAL_MAX = 65535 INTERVAL_MAX = 65535
INTERVAL_MIN = 1 INTERVAL_MIN = 1
@ -122,7 +124,7 @@ class PeriodTaskSerializerMixin(serializers.Serializer):
return crontab return crontab
def validate_interval(self, interval): def validate_interval(self, interval):
if not interval: if not interval and not isinstance(interval, int):
return interval return interval
msg = _("Range {} to {}").format(self.INTERVAL_MIN, self.INTERVAL_MAX) msg = _("Range {} to {}").format(self.INTERVAL_MIN, self.INTERVAL_MAX)
if interval > self.INTERVAL_MAX or interval < self.INTERVAL_MIN: if interval > self.INTERVAL_MAX or interval < self.INTERVAL_MIN:

View File

@ -60,7 +60,10 @@ class OrgViewSet(BulkModelViewSet):
@tmp_to_root_org() @tmp_to_root_org()
def get_data_from_model(self, model): def get_data_from_model(self, model):
if model == User: if model == User:
data = model.objects.filter(orgs__id=self.org.id, m2m_org_members__role=ROLE.USER) data = model.objects.filter(
orgs__id=self.org.id,
m2m_org_members__role__in=[ROLE.USER, ROLE.ADMIN, ROLE.AUDITOR]
)
elif model == Node: elif model == Node:
# 跟节点不能手动删除,所以排除检查 # 跟节点不能手动删除,所以排除检查
data = model.objects.filter(org_id=self.org.id).exclude(parent_key='', key__regex=r'^[0-9]+$') data = model.objects.filter(org_id=self.org.id).exclude(parent_key='', key__regex=r'^[0-9]+$')
@ -73,7 +76,7 @@ class OrgViewSet(BulkModelViewSet):
for model in org_related_models: for model in org_related_models:
data = self.get_data_from_model(model) data = self.get_data_from_model(model)
if data: if data:
msg = _(f'Have `{model._meta.verbose_name}` exists, Please delete') msg = _('Have {} exists, Please delete').format(model._meta.verbose_name)
return Response(data={'error': msg}, status=status.HTTP_403_FORBIDDEN) return Response(data={'error': msg}, status=status.HTTP_403_FORBIDDEN)
else: else:
if str(current_org) == str(self.org): if str(current_org) == str(self.org):

View File

@ -87,6 +87,8 @@ class OrgResourceStatisticsCache(OrgRelatedCache):
return users_amount return users_amount
def compute_assets_amount(self): def compute_assets_amount(self):
if self.org.is_root():
return Asset.objects.all().count()
node = Node.org_root() node = Node.org_root()
return node.assets_amount return node.assets_amount

View File

@ -18,6 +18,7 @@ def refresh_user_amount_on_user_create_or_delete(user_id):
for org in orgs: for org in orgs:
org_cache = OrgResourceStatisticsCache(org) org_cache = OrgResourceStatisticsCache(org)
org_cache.expire('users_amount') org_cache.expire('users_amount')
OrgResourceStatisticsCache(Organization.root()).expire('users_amount')
@receiver(post_save, sender=User) @receiver(post_save, sender=User)
@ -44,6 +45,7 @@ def on_org_user_changed_refresh_cache(sender, action, instance, reverse, pk_set,
for org in orgs: for org in orgs:
org_cache = OrgResourceStatisticsCache(org) org_cache = OrgResourceStatisticsCache(org)
org_cache.expire('users_amount') org_cache.expire('users_amount')
OrgResourceStatisticsCache(Organization.root()).expire('users_amount')
class OrgResourceStatisticsRefreshUtil: class OrgResourceStatisticsRefreshUtil:
@ -67,6 +69,7 @@ class OrgResourceStatisticsRefreshUtil:
if cache_field_name: if cache_field_name:
org_cache = OrgResourceStatisticsCache(instance.org) org_cache = OrgResourceStatisticsCache(instance.org)
org_cache.expire(*cache_field_name) org_cache.expire(*cache_field_name)
OrgResourceStatisticsCache(Organization.root()).expire(*cache_field_name)
@receiver(pre_save) @receiver(pre_save)

View File

@ -1,11 +1,12 @@
import requests
from rest_framework.views import Response from rest_framework.views import Response
from rest_framework.generics import GenericAPIView from rest_framework.generics import GenericAPIView
from rest_framework.exceptions import APIException
from rest_framework import status
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from settings.models import Setting
from common.permissions import IsSuperUser from common.permissions import IsSuperUser
from common.message.backends.dingtalk import URL from common.message.backends.dingtalk import DingTalk
from .. import serializers from .. import serializers
@ -20,19 +21,17 @@ class DingTalkTestingAPI(GenericAPIView):
dingtalk_appkey = serializer.validated_data['DINGTALK_APPKEY'] dingtalk_appkey = serializer.validated_data['DINGTALK_APPKEY']
dingtalk_agentid = serializer.validated_data['DINGTALK_AGENTID'] dingtalk_agentid = serializer.validated_data['DINGTALK_AGENTID']
dingtalk_appsecret = serializer.validated_data['DINGTALK_APPSECRET'] dingtalk_appsecret = serializer.validated_data.get('DINGTALK_APPSECRET')
if not dingtalk_appsecret:
secret = Setting.objects.filter(name='DINGTALK_APPSECRET').first()
if not secret:
return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': _('AppSecret is required')})
dingtalk_appsecret = secret.cleaned_value
try: try:
params = {'appkey': dingtalk_appkey, 'appsecret': dingtalk_appsecret} dingtalk = DingTalk(appid=dingtalk_appkey, appsecret=dingtalk_appsecret, agentid=dingtalk_agentid)
resp = requests.get(url=URL.GET_TOKEN, params=params) dingtalk.send_text(['test'], 'test')
if resp.status_code != 200: return Response(status=status.HTTP_200_OK, data={'msg': _('OK')})
return Response(status=400, data={'error': resp.json()}) except APIException as e:
return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': e.detail})
data = resp.json()
errcode = data['errcode']
if errcode != 0:
return Response(status=400, data={'error': data['errmsg']})
return Response(status=200, data={'msg': _('OK')})
except Exception as e:
return Response(status=400, data={'error': str(e)})

View File

@ -1,11 +1,12 @@
import requests
from rest_framework.views import Response from rest_framework.views import Response
from rest_framework.generics import GenericAPIView from rest_framework.generics import GenericAPIView
from rest_framework.exceptions import APIException
from rest_framework import status
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from settings.models import Setting
from common.permissions import IsSuperUser from common.permissions import IsSuperUser
from common.message.backends.wecom import URL from common.message.backends.wecom import WeCom
from .. import serializers from .. import serializers
@ -20,19 +21,17 @@ class WeComTestingAPI(GenericAPIView):
wecom_corpid = serializer.validated_data['WECOM_CORPID'] wecom_corpid = serializer.validated_data['WECOM_CORPID']
wecom_agentid = serializer.validated_data['WECOM_AGENTID'] wecom_agentid = serializer.validated_data['WECOM_AGENTID']
wecom_corpsecret = serializer.validated_data['WECOM_SECRET'] wecom_corpsecret = serializer.validated_data.get('WECOM_SECRET')
if not wecom_corpsecret:
secret = Setting.objects.filter(name='WECOM_SECRET').first()
if not secret:
return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': _('Secret is required')})
wecom_corpsecret = secret.cleaned_value
try: try:
params = {'corpid': wecom_corpid, 'corpsecret': wecom_corpsecret} wecom = WeCom(corpid=wecom_corpid, corpsecret=wecom_corpsecret, agentid=wecom_agentid)
resp = requests.get(url=URL.GET_TOKEN, params=params) wecom.send_text(['test'], 'test')
if resp.status_code != 200: return Response(status=status.HTTP_200_OK, data={'msg': _('OK')})
return Response(status=400, data={'error': resp.json()}) except APIException as e:
return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': e.detail})
data = resp.json()
errcode = data['errcode']
if errcode != 0:
return Response(status=400, data={'error': data['errmsg']})
return Response(status=200, data={'msg': _('OK')})
except Exception as e:
return Response(status=400, data={'error': str(e)})

View File

@ -170,7 +170,7 @@ class SecuritySettingSerializer(serializers.Serializer):
OLD_PASSWORD_HISTORY_LIMIT_COUNT = serializers.IntegerField( OLD_PASSWORD_HISTORY_LIMIT_COUNT = serializers.IntegerField(
min_value=0, max_value=99999, required=True, min_value=0, max_value=99999, required=True,
label=_('Number of repeated historical passwords'), label=_('Number of repeated historical passwords'),
help_text=_('Tip: When the user resets the password, it cannot be the previous n historical passwords of the user (the value of n here is the value filled in the input box)') help_text=_('Tip: When the user resets the password, it cannot be the previous n historical passwords of the user')
) )
SECURITY_PASSWORD_MIN_LENGTH = serializers.IntegerField( SECURITY_PASSWORD_MIN_LENGTH = serializers.IntegerField(
min_value=6, max_value=30, required=True, min_value=6, max_value=30, required=True,
@ -190,16 +190,16 @@ class SecuritySettingSerializer(serializers.Serializer):
class WeComSettingSerializer(serializers.Serializer): class WeComSettingSerializer(serializers.Serializer):
WECOM_CORPID = serializers.CharField(max_length=256, required=True, label=_('Corporation ID(corpid)')) WECOM_CORPID = serializers.CharField(max_length=256, required=True, label='corpid')
WECOM_AGENTID = serializers.CharField(max_length=256, required=True, label=_("Agent ID(agentid)")) WECOM_AGENTID = serializers.CharField(max_length=256, required=True, label='agentid')
WECOM_SECRET = serializers.CharField(max_length=256, required=True, label=_("Secret(secret)"), write_only=True) WECOM_SECRET = serializers.CharField(max_length=256, required=False, label='secret', write_only=True)
AUTH_WECOM = serializers.BooleanField(default=False, label=_('Enable WeCom Auth')) AUTH_WECOM = serializers.BooleanField(default=False, label=_('Enable WeCom Auth'))
class DingTalkSettingSerializer(serializers.Serializer): class DingTalkSettingSerializer(serializers.Serializer):
DINGTALK_AGENTID = serializers.CharField(max_length=256, required=True, label=_("AgentId")) DINGTALK_AGENTID = serializers.CharField(max_length=256, required=True, label='AgentId')
DINGTALK_APPKEY = serializers.CharField(max_length=256, required=True, label=_("AppKey")) DINGTALK_APPKEY = serializers.CharField(max_length=256, required=True, label='AppKey')
DINGTALK_APPSECRET = serializers.CharField(max_length=256, required=False, label=_("AppSecret"), write_only=True) DINGTALK_APPSECRET = serializers.CharField(max_length=256, required=False, label='AppSecret', write_only=True)
AUTH_DINGTALK = serializers.BooleanField(default=False, label=_('Enable DingTalk Auth')) AUTH_DINGTALK = serializers.BooleanField(default=False, label=_('Enable DingTalk Auth'))

View File

@ -90,7 +90,7 @@ class SessionViewSet(OrgBulkModelViewSet):
def filter_queryset(self, queryset): def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset) queryset = super().filter_queryset(queryset)
# 解决guacamole更新session时并发导致幽灵会话的问题 # 解决guacamole更新session时并发导致幽灵会话的问题,暂不处理
if self.request.method in ('PATCH',): if self.request.method in ('PATCH',):
queryset = queryset.select_for_update() queryset = queryset.select_for_update()
return queryset return queryset
@ -98,11 +98,6 @@ class SessionViewSet(OrgBulkModelViewSet):
def perform_create(self, serializer): def perform_create(self, serializer):
if hasattr(self.request.user, 'terminal'): if hasattr(self.request.user, 'terminal'):
serializer.validated_data["terminal"] = self.request.user.terminal serializer.validated_data["terminal"] = self.request.user.terminal
sid = serializer.validated_data["system_user"]
# guacamole提交的是id
if is_uuid(sid):
_system_user = get_object_or_404(SystemUser, id=sid)
serializer.validated_data["system_user"] = _system_user.name
return super().perform_create(serializer) return super().perform_create(serializer)
def get_permissions(self): def get_permissions(self):
@ -140,6 +135,7 @@ class SessionReplayViewSet(AsyncApiMixin, viewsets.ViewSet):
def get_replay_data(session, url): def get_replay_data(session, url):
tp = 'json' tp = 'json'
if session.protocol in ('rdp', 'vnc'): if session.protocol in ('rdp', 'vnc'):
# 需要考虑录像播放和离线播放器的约定,暂时不处理
tp = 'guacamole' tp = 'guacamole'
download_url = reverse('api-terminal:session-replay-download', kwargs={'pk': session.id}) download_url = reverse('api-terminal:session-replay-download', kwargs={'pk': session.id})

View File

@ -39,11 +39,6 @@ class StatusViewSet(viewsets.ModelViewSet):
def handle_sessions(self): def handle_sessions(self):
session_ids = self.request.data.get('sessions', []) session_ids = self.request.data.get('sessions', [])
# guacamole 上报的 session 是字符串
# "[53cd3e47-210f-41d8-b3c6-a184f3, 53cd3e47-210f-41d8-b3c6-a184f4]"
if isinstance(session_ids, str):
session_ids = session_ids[1:-1].split(',')
session_ids = [sid.strip() for sid in session_ids if sid.strip()]
Session.set_sessions_active(session_ids) Session.set_sessions_active(session_ids)
def get_queryset(self): def get_queryset(self):

View File

@ -43,6 +43,7 @@ class TerminalTypeChoices(TextChoices):
guacamole = 'guacamole', 'Guacamole' guacamole = 'guacamole', 'Guacamole'
omnidb = 'omnidb', 'OmniDB' omnidb = 'omnidb', 'OmniDB'
xrdp = 'xrdp', 'Xrdp' xrdp = 'xrdp', 'Xrdp'
lion = 'lion', 'Lion'
@classmethod @classmethod
def types(cls): def types(cls):

View File

@ -0,0 +1,18 @@
# Generated by Django 3.1.6 on 2021-05-17 06:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('terminal', '0034_auto_20210406_1434'),
]
operations = [
migrations.AlterField(
model_name='terminal',
name='type',
field=models.CharField(choices=[('koko', 'KoKo'), ('guacamole', 'Guacamole'), ('omnidb', 'OmniDB'), ('xrdp', 'Xrdp'), ('lion', 'Lion')], default='koko', max_length=64, verbose_name='type'),
),
]

View File

@ -109,7 +109,9 @@ class Session(OrgModelMixin):
_PROTOCOL = self.PROTOCOL _PROTOCOL = self.PROTOCOL
if self.is_finished: if self.is_finished:
return False return False
if self.protocol in [_PROTOCOL.SSH, _PROTOCOL.TELNET, _PROTOCOL.K8S]: if self.protocol in [
_PROTOCOL.SSH, _PROTOCOL.VNC, _PROTOCOL.RDP, _PROTOCOL.TELNET, _PROTOCOL.K8S
]:
return True return True
else: else:
return False return False

View File

@ -7,8 +7,6 @@ import string
import random import random
import datetime import datetime
from functools import partial
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.contrib.auth.hashers import check_password, make_password from django.contrib.auth.hashers import check_password, make_password
@ -30,7 +28,7 @@ from users.exceptions import MFANotEnabled
from ..signals import post_user_change_password from ..signals import post_user_change_password
__all__ = ['User'] __all__ = ['User', 'UserPasswordHistory']
logger = get_logger(__file__) logger = get_logger(__file__)
@ -83,12 +81,6 @@ class AuthMixin:
else: else:
return False return False
def save_history_password(self, password):
UserPasswordHistory.objects.create(
user=self, password=make_password(password),
date_created=self.date_password_last_updated
)
def is_public_key_valid(self): def is_public_key_valid(self):
""" """
Check if the user's ssh public key is valid. Check if the user's ssh public key is valid.
@ -771,3 +763,9 @@ class UserPasswordHistory(models.Model):
user = models.ForeignKey("users.User", related_name='history_passwords', user = models.ForeignKey("users.User", related_name='history_passwords',
on_delete=models.CASCADE, verbose_name=_('User')) on_delete=models.CASCADE, verbose_name=_('User'))
date_created = models.DateTimeField(auto_now_add=True, verbose_name=_("Date created")) date_created = models.DateTimeField(auto_now_add=True, verbose_name=_("Date created"))
def __str__(self):
return f'{self.user} set at {self.date_created}'
def __repr__(self):
return self.__str__()

View File

@ -39,8 +39,6 @@ class UserUpdatePasswordSerializer(serializers.ModelSerializer):
limit_count = settings.OLD_PASSWORD_HISTORY_LIMIT_COUNT limit_count = settings.OLD_PASSWORD_HISTORY_LIMIT_COUNT
msg = _('The new password cannot be the last {} passwords').format(limit_count) msg = _('The new password cannot be the last {} passwords').format(limit_count)
raise serializers.ValidationError(msg) raise serializers.ValidationError(msg)
else:
self.instance.save_history_password(value)
return value return value
def validate_new_password_again(self, value): def validate_new_password_again(self, value):

View File

@ -6,17 +6,33 @@ from django_auth_ldap.backend import populate_user
from django.conf import settings from django.conf import settings
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django_cas_ng.signals import cas_user_authenticated from django_cas_ng.signals import cas_user_authenticated
from django.db.models.signals import post_save
from jms_oidc_rp.signals import openid_create_or_update_user from jms_oidc_rp.signals import openid_create_or_update_user
from common.utils import get_logger from common.utils import get_logger
from .signals import post_user_create from .signals import post_user_create
from .models import User from .models import User, UserPasswordHistory
logger = get_logger(__file__) logger = get_logger(__file__)
@receiver(post_save, sender=User)
def save_passwd_change(sender, instance: User, **kwargs):
passwds = UserPasswordHistory.objects.filter(user=instance).order_by('-date_created')\
.values_list('password', flat=True)[:int(settings.OLD_PASSWORD_HISTORY_LIMIT_COUNT)]
for p in passwds:
if instance.password == p:
break
else:
UserPasswordHistory.objects.create(
user=instance, password=instance.password,
date_created=instance.date_password_last_updated
)
@receiver(post_user_create) @receiver(post_user_create)
def on_user_create(sender, user=None, **kwargs): def on_user_create(sender, user=None, **kwargs):
logger.debug("Receive user `{}` create signal".format(user.name)) logger.debug("Receive user `{}` create signal".format(user.name))

View File

@ -111,8 +111,6 @@ class UserResetPasswordView(FormView):
error = _('* The new password cannot be the last {} passwords').format(limit_count) error = _('* The new password cannot be the last {} passwords').format(limit_count)
form.add_error('new_password', error) form.add_error('new_password', error)
return self.form_invalid(form) return self.form_invalid(form)
else:
user.save_history_password(password)
user.reset_password(password) user.reset_password(password)
User.expired_reset_password_token(token) User.expired_reset_password_token(token)