diff --git a/apps/applications/serializers/attrs/application_category/remote_app.py b/apps/applications/serializers/attrs/application_category/remote_app.py index 9fa691976..3cda4ac1e 100644 --- a/apps/applications/serializers/attrs/application_category/remote_app.py +++ b/apps/applications/serializers/attrs/application_category/remote_app.py @@ -39,14 +39,14 @@ class RemoteAppSerializer(serializers.Serializer): @staticmethod def get_asset_info(obj): 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 {} 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: logger.error(e) return {} if not asset: return {} - asset_info = {'id': str(asset[0]), 'hostname': asset[1]} + asset_info = {'id': str(asset.id), 'hostname': asset.hostname} return asset_info diff --git a/apps/authentication/api/dingtalk.py b/apps/authentication/api/dingtalk.py index e4b2ea85b..ce1732118 100644 --- a/apps/authentication/api/dingtalk.py +++ b/apps/authentication/api/dingtalk.py @@ -21,7 +21,7 @@ class DingTalkQRUnBindBase(APIView): if not user.dingtalk_id: raise errors.DingTalkNotBound - user.dingtalk_id = '' + user.dingtalk_id = None user.save() return Response() diff --git a/apps/authentication/api/wecom.py b/apps/authentication/api/wecom.py index 1ab5ff725..c66da5f79 100644 --- a/apps/authentication/api/wecom.py +++ b/apps/authentication/api/wecom.py @@ -21,7 +21,7 @@ class WeComQRUnBindBase(APIView): if not user.wecom_id: raise errors.WeComNotBound - user.wecom_id = '' + user.wecom_id = None user.save() return Response() diff --git a/apps/authentication/backends/api.py b/apps/authentication/backends/api.py index 63356eff6..9da3bbbc3 100644 --- a/apps/authentication/backends/api.py +++ b/apps/authentication/backends/api.py @@ -8,7 +8,7 @@ from django.core.cache import cache from django.utils.translation import ugettext as _ from six import text_type 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 authentication, exceptions from common.auth import signature @@ -25,6 +25,11 @@ def get_request_date_header(request): return date +class ModelBackend(DJModelBackend): + def user_can_authenticate(self, user): + return user.is_valid + + class AccessKeyAuthentication(authentication.BaseAuthentication): """App使用Access key进行签名认证, 目前签名算法比较简单, app注册或者手动建立后,会生成 access_key_id 和 access_key_secret, diff --git a/apps/authentication/views/dingtalk.py b/apps/authentication/views/dingtalk.py index c2e139467..24861b979 100644 --- a/apps/authentication/views/dingtalk.py +++ b/apps/authentication/views/dingtalk.py @@ -8,7 +8,9 @@ from django.views.generic import TemplateView from django.views import View from django.conf import settings from django.http.request import HttpRequest +from django.db.utils import IntegrityError from rest_framework.permissions import IsAuthenticated, AllowAny +from rest_framework.exceptions import APIException from users.views import UserVerifyPasswordView from users.utils import is_auth_password_time_valid @@ -29,6 +31,20 @@ DINGTALK_STATE_SESSION_KEY = '_dingtalk_state' 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): state = self.request.GET.get('state') 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) return response - user.dingtalk_id = userid - user.save() + try: + user.dingtalk_id = userid + 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') response = self.get_success_reponse(redirect_url, msg, msg) diff --git a/apps/authentication/views/wecom.py b/apps/authentication/views/wecom.py index 097df8e95..981c12508 100644 --- a/apps/authentication/views/wecom.py +++ b/apps/authentication/views/wecom.py @@ -8,7 +8,9 @@ from django.views.generic import TemplateView from django.views import View from django.conf import settings from django.http.request import HttpRequest +from django.db.utils import IntegrityError from rest_framework.permissions import IsAuthenticated, AllowAny +from rest_framework.exceptions import APIException from users.views import UserVerifyPasswordView from users.utils import is_auth_password_time_valid @@ -29,6 +31,20 @@ WECOM_STATE_SESSION_KEY = '_wecom_state' 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): state = self.request.GET.get('state') 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) return response - user.wecom_id = wecom_userid - user.save() + try: + user.wecom_id = wecom_userid + 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') response = self.get_success_reponse(redirect_url, msg, msg) diff --git a/apps/common/drf/parsers/base.py b/apps/common/drf/parsers/base.py index f228960f0..acffcfef8 100644 --- a/apps/common/drf/parsers/base.py +++ b/apps/common/drf/parsers/base.py @@ -57,6 +57,8 @@ class BaseFileParser(BaseParser): @staticmethod def _replace_chinese_quote(s): + if not isinstance(s, str): + return s trans_table = str.maketrans({ '“': '"', '”': '"', diff --git a/apps/common/message/backends/exceptions.py b/apps/common/message/backends/exceptions.py index f72e8694d..e28a80811 100644 --- a/apps/common/message/backends/exceptions.py +++ b/apps/common/message/backends/exceptions.py @@ -21,8 +21,3 @@ class ResponseDataKeyError(APIException): class NetError(APIException): default_code = 'net_error' default_detail = _('Network error, please contact system administrator') - - -class AccessTokenError(APIException): - default_code = 'access_token_error' - default_detail = 'Access token error, check config' diff --git a/apps/common/message/backends/mixin.py b/apps/common/message/backends/mixin.py index 3beb60272..5652a1520 100644 --- a/apps/common/message/backends/mixin.py +++ b/apps/common/message/backends/mixin.py @@ -22,7 +22,7 @@ class RequestMixin: logger.error(f'Response 200 but errcode is not 0: ' f'errcode={errcode} ' 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): if response.status_code != 200: @@ -31,7 +31,7 @@ class RequestMixin: f'status_code={response.status_code} ' f'url={response.url}' f'\ncontent={response.content}') - raise exce.HTTPNot200 + raise exce.HTTPNot200(detail=response.json()) class BaseRequest(RequestMixin): diff --git a/apps/common/message/backends/utils.py b/apps/common/message/backends/utils.py index 6c6f2b593..5a2f90355 100644 --- a/apps/common/message/backends/utils.py +++ b/apps/common/message/backends/utils.py @@ -39,7 +39,7 @@ class DictWrapper: except KeyError as e: msg = f'Response 200 but get field from json error: error={e} data={self.raw_data}' logger.error(msg) - raise exce.ResponseDataKeyError(detail=msg) + raise exce.ResponseDataKeyError(detail=self.raw_data) def __getattr__(self, item): return getattr(self.raw_data, item) diff --git a/apps/common/message/backends/wecom/__init__.py b/apps/common/message/backends/wecom/__init__.py index 257da47a0..dd3e34c8a 100644 --- a/apps/common/message/backends/wecom/__init__.py +++ b/apps/common/message/backends/wecom/__init__.py @@ -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 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 INVALID_CODE = 40029 @@ -141,7 +144,7 @@ class WeCom(RequestMixin): data = self._requests.post(URL.SEND_MESSAGE, json=body, check_errcode_is_0=False) errcode = data['errcode'] - if errcode == ErrorCode.RECIPIENTS_INVALID: + if errcode in (ErrorCode.RECIPIENTS_INVALID, ErrorCode.RECIPIENTS_EMPTY): # 全部接收人无权限或不存在 return users self.check_errcode_is_0(data) diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py index 9fe5886c0..502146f13 100644 --- a/apps/jumpserver/settings/auth.py +++ b/apps/jumpserver/settings/auth.py @@ -120,7 +120,7 @@ LOGIN_CONFIRM_ENABLE = CONFIG.LOGIN_CONFIRM_ENABLE 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_LDAP = 'authentication.backends.ldap.LDAPAuthorizationBackend' AUTH_BACKEND_OIDC_PASSWORD = 'jms_oidc_rp.backends.OIDCAuthPasswordBackend' diff --git a/apps/jumpserver/settings/base.py b/apps/jumpserver/settings/base.py index d4a8ce27a..4a2e59062 100644 --- a/apps/jumpserver/settings/base.py +++ b/apps/jumpserver/settings/base.py @@ -16,7 +16,7 @@ PROJECT_DIR = const.PROJECT_DIR # SECURITY WARNING: keep the secret key used in production secret! 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 # SECURITY WARNING: don't run with debug turned on in production! diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 304516e3c..1272a409d 100644 Binary files a/apps/locale/zh/LC_MESSAGES/django.mo and b/apps/locale/zh/LC_MESSAGES/django.mo differ diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 1abbfdd86..1434212a1 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\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" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -1547,44 +1547,48 @@ msgstr "返回" msgid "Copy success" 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" msgstr "你被攻击了" -#: authentication/views/dingtalk.py:76 +#: authentication/views/dingtalk.py:77 msgid "DingTalk is already bound" 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" 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" msgstr "无效的 user_id" -#: authentication/views/dingtalk.py:129 +#: authentication/views/dingtalk.py:130 msgid "DingTalk query user failed" msgstr "钉钉查询用户失败" -#: authentication/views/dingtalk.py:136 authentication/views/dingtalk.py:219 -#: authentication/views/dingtalk.py:220 +#: authentication/views/dingtalk.py:139 +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" msgstr "绑定 钉钉 成功" -#: authentication/views/dingtalk.py:188 +#: authentication/views/dingtalk.py:196 msgid "Failed to get user from DingTalk" msgstr "从钉钉获取用户失败" -#: authentication/views/dingtalk.py:194 +#: authentication/views/dingtalk.py:202 msgid "DingTalk is not bound" 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" 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" msgstr "绑定钉钉失败" @@ -1620,28 +1624,32 @@ msgstr "退出登录成功" msgid "Logout success, return login page" msgstr "退出登录成功,返回到登录页面" -#: authentication/views/wecom.py:75 +#: authentication/views/wecom.py:76 msgid "WeCom is already bound" msgstr "企业微信已经绑定" -#: authentication/views/wecom.py:127 +#: authentication/views/wecom.py:128 msgid "WeCom query user failed" msgstr "企业微信查询用户失败" -#: authentication/views/wecom.py:134 authentication/views/wecom.py:217 -#: authentication/views/wecom.py:218 +#: authentication/views/wecom.py:137 +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" msgstr "绑定 企业微信 成功" -#: authentication/views/wecom.py:186 +#: authentication/views/wecom.py:194 msgid "Failed to get user from WeCom" msgstr "从企业微信获取用户失败" -#: authentication/views/wecom.py:192 +#: authentication/views/wecom.py:200 msgid "WeCom is not bound" 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" msgstr "绑定企业微信失败" @@ -1803,36 +1811,36 @@ msgstr "没有该主机 {} 权限" msgid "Operations" 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" 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" 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 msgid "Periodic perform" msgstr "定时执行" -#: ops/mixin.py:111 +#: ops/mixin.py:112 msgid "Interval" msgstr "间隔" -#: ops/mixin.py:120 +#: ops/mixin.py:122 msgid "* Please enter a valid crontab expression" msgstr "* 请输入有效的 crontab 表达式" -#: ops/mixin.py:127 +#: ops/mixin.py:129 msgid "Range {} to {}" msgstr "输入在 {} - {} 范围之间" -#: ops/mixin.py:138 +#: ops/mixin.py:140 msgid "Require periodic or regularly perform setting" msgstr "需要周期或定期设置" -#: ops/mixin.py:149 +#: ops/mixin.py:151 msgid "" "eg: Every Sunday 03:05 run <5 3 * * 0>
Tips: Using 5 digits linux " "crontab expressions (在线工" "具
注意: 如果同时设置了定期执行和周期执行,优先使用定期执行" -#: ops/mixin.py:160 +#: ops/mixin.py:162 msgid "Tips: (Units: hour)" msgstr "提示:(单位: 时)" @@ -1954,12 +1962,11 @@ msgstr "更新任务内容: {}" msgid "Disk used more than 80%: {} => {}" msgstr "磁盘使用率超过 80%: {} => {}" -#: orgs/api.py:76 -#, python-brace-format -msgid "Have `{model._meta.verbose_name}` exists, Please delete" -msgstr "`{model._meta.verbose_name}` 存在数据, 请先删除" +#: orgs/api.py:79 +msgid "Have {} exists, Please delete" +msgstr "{} 存在数据, 请先删除" -#: orgs/api.py:80 +#: orgs/api.py:83 msgid "The current organization cannot be deleted" msgstr "当前组织不能被删除" @@ -2123,7 +2130,11 @@ msgstr "邮件已经发送{}, 请检查" msgid "Welcome to the JumpServer open source Bastion Host" 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" msgstr "" @@ -2135,6 +2146,10 @@ msgstr "获取 LDAP 用户为 None" msgid "Imported {} users successfully" msgstr "导入 {} 个用户成功" +#: settings/api/wecom.py:29 +msgid "Secret is required" +msgstr "Secret 是必须的" + #: settings/models.py:123 users/templates/users/reset_password.html:29 msgid "Setting" msgstr "设置" @@ -2419,16 +2434,13 @@ msgstr "" #: settings/serializers/settings.py:172 msgid "Number of repeated historical passwords" -msgstr "历史密码可重复次数" +msgstr "不能设置近几次密码" #: settings/serializers/settings.py:173 msgid "" "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)" -msgstr "" -"提示:用户重置密码时,不能为该用户前n次历史密码 (此处的n值即为输入框中填写的" -"值)" +"historical passwords of the user" +msgstr "提示:用户重置密码时,不能为该用户前几次使用过的密码" #: settings/serializers/settings.py:177 msgid "Password minimum length" @@ -2462,34 +2474,10 @@ msgstr "邮件收件人" msgid "Multiple user using , split" 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 msgid "Enable WeCom Auth" 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 msgid "Enable DingTalk Auth" msgstr "启用钉钉认证" @@ -2896,7 +2884,7 @@ msgstr "取消" #: templates/flash_message_standalone.html:37 msgid "Go" -msgstr "" +msgstr "立即" #: templates/index.html:11 msgid "Total users" @@ -3048,19 +3036,19 @@ msgstr "登录了" msgid "Filters" msgstr "过滤" -#: terminal/api/session.py:189 +#: terminal/api/session.py:185 msgid "Session does not exist: {}" msgstr "会话不存在: {}" -#: terminal/api/session.py:192 +#: terminal/api/session.py:188 msgid "Session is finished or the protocol not supported" msgstr "会话已经完成或协议不支持" -#: terminal/api/session.py:197 +#: terminal/api/session.py:193 msgid "User does not exist: {}" msgstr "用户不存在: {}" -#: terminal/api/session.py:201 +#: terminal/api/session.py:197 msgid "User does not have permission" msgstr "用户没有权限" @@ -3094,7 +3082,7 @@ msgstr "测试失败: 账户无效" #: terminal/backends/command/es.py:27 msgid "Invalid elasticsearch config" -msgstr "" +msgstr "无效的 Elasticsearch 配置" #: terminal/backends/command/models.py:14 msgid "Ordinary" @@ -4953,7 +4941,7 @@ msgstr "实例个数" msgid "Periodic display" msgstr "定时执行" -#: xpack/plugins/cloud/utils.py:65 +#: xpack/plugins/cloud/utils.py:64 msgid "Account unavailable" msgstr "账户无效" @@ -5041,5 +5029,23 @@ msgstr "旗舰版" msgid "Community edition" 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" #~ msgstr "无" diff --git a/apps/ops/mixin.py b/apps/ops/mixin.py index 166c5db75..c6f1ce184 100644 --- a/apps/ops/mixin.py +++ b/apps/ops/mixin.py @@ -103,12 +103,14 @@ class PeriodTaskModelMixin(models.Model): 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( max_length=128, allow_blank=True, 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_MIN = 1 @@ -122,7 +124,7 @@ class PeriodTaskSerializerMixin(serializers.Serializer): return crontab def validate_interval(self, interval): - if not interval: + if not interval and not isinstance(interval, int): return interval msg = _("Range {} to {}").format(self.INTERVAL_MIN, self.INTERVAL_MAX) if interval > self.INTERVAL_MAX or interval < self.INTERVAL_MIN: diff --git a/apps/orgs/api.py b/apps/orgs/api.py index b241301a8..ace14112b 100644 --- a/apps/orgs/api.py +++ b/apps/orgs/api.py @@ -60,7 +60,10 @@ class OrgViewSet(BulkModelViewSet): @tmp_to_root_org() def get_data_from_model(self, model): 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: # 跟节点不能手动删除,所以排除检查 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: data = self.get_data_from_model(model) 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) else: if str(current_org) == str(self.org): diff --git a/apps/orgs/caches.py b/apps/orgs/caches.py index c9d60bd18..8f73dc06e 100644 --- a/apps/orgs/caches.py +++ b/apps/orgs/caches.py @@ -87,6 +87,8 @@ class OrgResourceStatisticsCache(OrgRelatedCache): return users_amount def compute_assets_amount(self): + if self.org.is_root(): + return Asset.objects.all().count() node = Node.org_root() return node.assets_amount diff --git a/apps/orgs/signals_handler/cache.py b/apps/orgs/signals_handler/cache.py index 5862d5edf..626975991 100644 --- a/apps/orgs/signals_handler/cache.py +++ b/apps/orgs/signals_handler/cache.py @@ -18,6 +18,7 @@ def refresh_user_amount_on_user_create_or_delete(user_id): for org in orgs: org_cache = OrgResourceStatisticsCache(org) org_cache.expire('users_amount') + OrgResourceStatisticsCache(Organization.root()).expire('users_amount') @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: org_cache = OrgResourceStatisticsCache(org) org_cache.expire('users_amount') + OrgResourceStatisticsCache(Organization.root()).expire('users_amount') class OrgResourceStatisticsRefreshUtil: @@ -67,6 +69,7 @@ class OrgResourceStatisticsRefreshUtil: if cache_field_name: org_cache = OrgResourceStatisticsCache(instance.org) org_cache.expire(*cache_field_name) + OrgResourceStatisticsCache(Organization.root()).expire(*cache_field_name) @receiver(pre_save) diff --git a/apps/settings/api/dingtalk.py b/apps/settings/api/dingtalk.py index e560f8626..4e4a73bf7 100644 --- a/apps/settings/api/dingtalk.py +++ b/apps/settings/api/dingtalk.py @@ -1,11 +1,12 @@ -import requests - from rest_framework.views import Response 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 settings.models import Setting from common.permissions import IsSuperUser -from common.message.backends.dingtalk import URL +from common.message.backends.dingtalk import DingTalk from .. import serializers @@ -20,19 +21,17 @@ class DingTalkTestingAPI(GenericAPIView): dingtalk_appkey = serializer.validated_data['DINGTALK_APPKEY'] 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: - params = {'appkey': dingtalk_appkey, 'appsecret': dingtalk_appsecret} - resp = requests.get(url=URL.GET_TOKEN, params=params) - if resp.status_code != 200: - return Response(status=400, data={'error': resp.json()}) - - 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)}) + dingtalk = DingTalk(appid=dingtalk_appkey, appsecret=dingtalk_appsecret, agentid=dingtalk_agentid) + dingtalk.send_text(['test'], 'test') + return Response(status=status.HTTP_200_OK, data={'msg': _('OK')}) + except APIException as e: + return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': e.detail}) diff --git a/apps/settings/api/wecom.py b/apps/settings/api/wecom.py index 3087efd1f..5059b7647 100644 --- a/apps/settings/api/wecom.py +++ b/apps/settings/api/wecom.py @@ -1,11 +1,12 @@ -import requests - from rest_framework.views import Response 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 settings.models import Setting from common.permissions import IsSuperUser -from common.message.backends.wecom import URL +from common.message.backends.wecom import WeCom from .. import serializers @@ -20,19 +21,17 @@ class WeComTestingAPI(GenericAPIView): wecom_corpid = serializer.validated_data['WECOM_CORPID'] 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: - params = {'corpid': wecom_corpid, 'corpsecret': wecom_corpsecret} - resp = requests.get(url=URL.GET_TOKEN, params=params) - if resp.status_code != 200: - return Response(status=400, data={'error': resp.json()}) - - 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)}) + wecom = WeCom(corpid=wecom_corpid, corpsecret=wecom_corpsecret, agentid=wecom_agentid) + wecom.send_text(['test'], 'test') + return Response(status=status.HTTP_200_OK, data={'msg': _('OK')}) + except APIException as e: + return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': e.detail}) diff --git a/apps/settings/serializers/settings.py b/apps/settings/serializers/settings.py index 3aca30c10..f26679c29 100644 --- a/apps/settings/serializers/settings.py +++ b/apps/settings/serializers/settings.py @@ -170,7 +170,7 @@ class SecuritySettingSerializer(serializers.Serializer): OLD_PASSWORD_HISTORY_LIMIT_COUNT = serializers.IntegerField( min_value=0, max_value=99999, required=True, 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( min_value=6, max_value=30, required=True, @@ -190,16 +190,16 @@ class SecuritySettingSerializer(serializers.Serializer): class WeComSettingSerializer(serializers.Serializer): - WECOM_CORPID = serializers.CharField(max_length=256, required=True, label=_('Corporation ID(corpid)')) - WECOM_AGENTID = serializers.CharField(max_length=256, required=True, label=_("Agent ID(agentid)")) - WECOM_SECRET = serializers.CharField(max_length=256, required=True, label=_("Secret(secret)"), write_only=True) + WECOM_CORPID = serializers.CharField(max_length=256, required=True, label='corpid') + WECOM_AGENTID = serializers.CharField(max_length=256, required=True, label='agentid') + WECOM_SECRET = serializers.CharField(max_length=256, required=False, label='secret', write_only=True) AUTH_WECOM = serializers.BooleanField(default=False, label=_('Enable WeCom Auth')) class DingTalkSettingSerializer(serializers.Serializer): - DINGTALK_AGENTID = serializers.CharField(max_length=256, required=True, label=_("AgentId")) - 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_AGENTID = serializers.CharField(max_length=256, required=True, label='AgentId') + 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) AUTH_DINGTALK = serializers.BooleanField(default=False, label=_('Enable DingTalk Auth')) diff --git a/apps/terminal/api/session.py b/apps/terminal/api/session.py index 2a749aac5..359ecffc2 100644 --- a/apps/terminal/api/session.py +++ b/apps/terminal/api/session.py @@ -90,7 +90,7 @@ class SessionViewSet(OrgBulkModelViewSet): def filter_queryset(self, queryset): queryset = super().filter_queryset(queryset) - # 解决guacamole更新session时并发导致幽灵会话的问题 + # 解决guacamole更新session时并发导致幽灵会话的问题,暂不处理 if self.request.method in ('PATCH',): queryset = queryset.select_for_update() return queryset @@ -98,11 +98,6 @@ class SessionViewSet(OrgBulkModelViewSet): def perform_create(self, serializer): if hasattr(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) def get_permissions(self): @@ -140,6 +135,7 @@ class SessionReplayViewSet(AsyncApiMixin, viewsets.ViewSet): def get_replay_data(session, url): tp = 'json' if session.protocol in ('rdp', 'vnc'): + # 需要考虑录像播放和离线播放器的约定,暂时不处理 tp = 'guacamole' download_url = reverse('api-terminal:session-replay-download', kwargs={'pk': session.id}) diff --git a/apps/terminal/api/status.py b/apps/terminal/api/status.py index b39e13ba7..fdf3ef1f1 100644 --- a/apps/terminal/api/status.py +++ b/apps/terminal/api/status.py @@ -39,11 +39,6 @@ class StatusViewSet(viewsets.ModelViewSet): def handle_sessions(self): 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) def get_queryset(self): diff --git a/apps/terminal/const.py b/apps/terminal/const.py index 830913e28..ff638325d 100644 --- a/apps/terminal/const.py +++ b/apps/terminal/const.py @@ -43,6 +43,7 @@ class TerminalTypeChoices(TextChoices): guacamole = 'guacamole', 'Guacamole' omnidb = 'omnidb', 'OmniDB' xrdp = 'xrdp', 'Xrdp' + lion = 'lion', 'Lion' @classmethod def types(cls): diff --git a/apps/terminal/migrations/0035_auto_20210517_1448.py b/apps/terminal/migrations/0035_auto_20210517_1448.py new file mode 100644 index 000000000..2c8592700 --- /dev/null +++ b/apps/terminal/migrations/0035_auto_20210517_1448.py @@ -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'), + ), + ] diff --git a/apps/terminal/models/session.py b/apps/terminal/models/session.py index 6d85759af..86843433e 100644 --- a/apps/terminal/models/session.py +++ b/apps/terminal/models/session.py @@ -109,7 +109,9 @@ class Session(OrgModelMixin): _PROTOCOL = self.PROTOCOL if self.is_finished: 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 else: return False diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 715f02b9d..97a9e3d6d 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -7,8 +7,6 @@ import string import random import datetime -from functools import partial - from django.conf import settings from django.contrib.auth.models import AbstractUser 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 -__all__ = ['User'] +__all__ = ['User', 'UserPasswordHistory'] logger = get_logger(__file__) @@ -83,12 +81,6 @@ class AuthMixin: else: 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): """ 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', on_delete=models.CASCADE, verbose_name=_('User')) 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__() diff --git a/apps/users/serializers/profile.py b/apps/users/serializers/profile.py index 261dd6f01..c1caf1f99 100644 --- a/apps/users/serializers/profile.py +++ b/apps/users/serializers/profile.py @@ -39,8 +39,6 @@ class UserUpdatePasswordSerializer(serializers.ModelSerializer): limit_count = settings.OLD_PASSWORD_HISTORY_LIMIT_COUNT msg = _('The new password cannot be the last {} passwords').format(limit_count) raise serializers.ValidationError(msg) - else: - self.instance.save_history_password(value) return value def validate_new_password_again(self, value): diff --git a/apps/users/signals_handler.py b/apps/users/signals_handler.py index 887531f45..9831367fc 100644 --- a/apps/users/signals_handler.py +++ b/apps/users/signals_handler.py @@ -6,17 +6,33 @@ from django_auth_ldap.backend import populate_user from django.conf import settings from django.core.exceptions import PermissionDenied 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 common.utils import get_logger from .signals import post_user_create -from .models import User +from .models import User, UserPasswordHistory 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) def on_user_create(sender, user=None, **kwargs): logger.debug("Receive user `{}` create signal".format(user.name)) diff --git a/apps/users/views/profile/reset.py b/apps/users/views/profile/reset.py index 356694020..ba9cfd9b7 100644 --- a/apps/users/views/profile/reset.py +++ b/apps/users/views/profile/reset.py @@ -111,8 +111,6 @@ class UserResetPasswordView(FormView): error = _('* The new password cannot be the last {} passwords').format(limit_count) form.add_error('new_password', error) return self.form_invalid(form) - else: - user.save_history_password(password) user.reset_password(password) User.expired_reset_password_token(token)