diff --git a/apps/assets/api/domain.py b/apps/assets/api/domain.py index 13be62315..e6f6c0550 100644 --- a/apps/assets/api/domain.py +++ b/apps/assets/api/domain.py @@ -44,4 +44,4 @@ class GatewayTestConnectionApi(SingleObjectMixin, APIView): if ok: return Response("ok") else: - return Response({"failed": e}, status=404) + return Response({"error": e}, status=400) diff --git a/apps/assets/api/system_user.py b/apps/assets/api/system_user.py index 5bf38853d..f1213f0a3 100644 --- a/apps/assets/api/system_user.py +++ b/apps/assets/api/system_user.py @@ -14,11 +14,12 @@ # limitations under the License. from django.shortcuts import get_object_or_404 +from django.conf import settings from rest_framework.response import Response from common.serializers import CeleryTaskSerializer from common.utils import get_logger -from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser +from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsAppUser from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins import generics from ..models import SystemUser, Asset @@ -69,7 +70,7 @@ class SystemUserAssetAuthInfoApi(generics.RetrieveAPIView): Get system user with asset auth info """ model = SystemUser - permission_classes = (IsOrgAdminOrAppUser,) + permission_classes = (IsAppUser,) serializer_class = serializers.SystemUserAuthSerializer def get_object(self): diff --git a/apps/assets/models/domain.py b/apps/assets/models/domain.py index 2051181fe..99e677a07 100644 --- a/apps/assets/models/domain.py +++ b/apps/assets/models/domain.py @@ -3,9 +3,9 @@ import uuid import random +import re import paramiko - from django.db import models from django.utils.translation import ugettext_lazy as _ @@ -63,6 +63,9 @@ class Gateway(AssetUser): def test_connective(self, local_port=None): if local_port is None: local_port = self.port + if not re.match(r'\w+$', self.password): + return False, _("Password should not contain special characters") + client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) proxy = paramiko.SSHClient() diff --git a/apps/assets/models/node.py b/apps/assets/models/node.py index e6ceb1f66..f349eb9f6 100644 --- a/apps/assets/models/node.py +++ b/apps/assets/models/node.py @@ -68,7 +68,7 @@ class TreeMixin: @classmethod def refresh_node_assets(cls, t=None): - logger.debug("Refresh node tree assets") + logger.debug("Refresh node assets") key = cls.tree_assets_cache_key ttl = cls.tree_cache_time if not t: diff --git a/apps/assets/templates/assets/_asset_list_modal.html b/apps/assets/templates/assets/_asset_list_modal.html index 8d8c3f0ba..4c6eb7199 100644 --- a/apps/assets/templates/assets/_asset_list_modal.html +++ b/apps/assets/templates/assets/_asset_list_modal.html @@ -25,7 +25,7 @@
-
+
diff --git a/apps/assets/templates/assets/_node_tree.html b/apps/assets/templates/assets/_node_tree.html index c76d3d685..803c29c13 100644 --- a/apps/assets/templates/assets/_node_tree.html +++ b/apps/assets/templates/assets/_node_tree.html @@ -32,8 +32,7 @@ } - -
+
@@ -306,6 +305,7 @@ function defaultCallback(action) { $(document).ready(function () { + $('.treebox').css('height', window.innerHeight - 180); }) .on('click', '.btn-show-current-asset', function(){ hideRMenu(); @@ -322,4 +322,4 @@ $(document).ready(function () { location.reload(); }) - \ No newline at end of file + diff --git a/apps/assets/templates/assets/asset_list.html b/apps/assets/templates/assets/asset_list.html index be267cc4d..fcc17c87d 100644 --- a/apps/assets/templates/assets/asset_list.html +++ b/apps/assets/templates/assets/asset_list.html @@ -426,13 +426,15 @@ $(document).ready(function(){ function success(data) { url = setUrlParam(the_url, 'spm', data.spm); requestApi({ - url:url, - method:'DELETE', - success:refreshPage, - flash_message:false, + url: url, + method: 'DELETE', + success: function () { + var msg = "{% trans 'Asset Deleted.' %}"; + swal("{% trans 'Asset Delete' %}", msg, "success"); + refreshPage(); + }, + flash_message: false, }); - var msg = "{% trans 'Asset Deleted.' %}"; - swal("{% trans 'Asset Delete' %}", msg, "success"); } function fail() { var msg = "{% trans 'Asset Deleting failed.' %}"; @@ -440,10 +442,11 @@ $(document).ready(function(){ } requestApi({ url: "{% url 'api-common:resources-cache' %}", - method:'POST', - body:JSON.stringify(data), - success:success, - error:fail + method: 'POST', + body: JSON.stringify(data), + success: success, + error: fail, + flash_message: false }) }) } diff --git a/apps/assets/templates/assets/domain_gateway_list.html b/apps/assets/templates/assets/domain_gateway_list.html index ade46acc9..79636a2bc 100644 --- a/apps/assets/templates/assets/domain_gateway_list.html +++ b/apps/assets/templates/assets/domain_gateway_list.html @@ -139,7 +139,7 @@ $(document).ready(function(){ method: "POST", body: JSON.stringify({'port': parseInt(data.port)}), success_message: "{% trans 'Can be connected' %}", - fail_message: "{% trans 'The connection fails' %}" + {#fail_message: "{% trans 'The connection fails' %}"#} }) }); diff --git a/apps/authentication/signals_handlers.py b/apps/authentication/signals_handlers.py index c0b48c61d..b894e0651 100644 --- a/apps/authentication/signals_handlers.py +++ b/apps/authentication/signals_handlers.py @@ -47,7 +47,7 @@ def on_openid_login_success(sender, user=None, request=None, **kwargs): @receiver(populate_user) def on_ldap_create_user(sender, user, ldap_user, **kwargs): - if user and user.username != 'admin': + if user and user.username not in ['admin']: user.source = user.SOURCE_LDAP user.save() diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 58ca170c6..f7f35c215 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -395,6 +395,7 @@ defaults = { 'FLOWER_URL': "127.0.0.1:5555", 'DEFAULT_ORG_SHOW_ALL_USERS': True, 'PERIOD_TASK_ENABLED': True, + 'WINDOWS_SKIP_ALL_MANUAL_PASSWORD': False, } diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py index f6b81e877..fe4f2fc84 100644 --- a/apps/jumpserver/settings.py +++ b/apps/jumpserver/settings.py @@ -516,7 +516,7 @@ CELERY_TASK_EAGER_PROPAGATES = True CELERY_WORKER_REDIRECT_STDOUTS = True CELERY_WORKER_REDIRECT_STDOUTS_LEVEL = "INFO" # CELERY_WORKER_HIJACK_ROOT_LOGGER = True -CELERY_WORKER_MAX_TASKS_PER_CHILD = 40 +# CELERY_WORKER_MAX_TASKS_PER_CHILD = 40 CELERY_TASK_SOFT_TIME_LIMIT = 3600 # Cache use redis diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index a6510ca5a..233d13246 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 fdc37654f..95f544397 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: 2019-10-25 10:52+0800\n" +"POT-Creation-Date: 2019-11-13 16:38+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: Jumpserver team\n" @@ -83,7 +83,7 @@ msgstr "运行参数" #: assets/templates/assets/domain_detail.html:60 #: assets/templates/assets/domain_list.html:26 #: assets/templates/assets/label_list.html:16 -#: assets/templates/assets/system_user_list.html:51 audits/models.py:19 +#: assets/templates/assets/system_user_list.html:51 audits/models.py:20 #: audits/templates/audits/ftp_log_list.html:44 #: audits/templates/audits/ftp_log_list.html:74 #: perms/forms/asset_permission.py:84 perms/models/asset_permission.py:80 @@ -137,7 +137,7 @@ msgstr "资产" #: perms/templates/perms/remote_app_permission_remote_app.html:53 #: perms/templates/perms/remote_app_permission_user.html:53 #: settings/models.py:29 -#: settings/templates/settings/_ldap_list_users_modal.html:31 +#: settings/templates/settings/_ldap_list_users_modal.html:32 #: settings/templates/settings/command_storage_create.html:41 #: settings/templates/settings/replay_storage_create.html:44 #: settings/templates/settings/terminal_setting.html:83 @@ -197,7 +197,7 @@ msgstr "参数" #: orgs/models.py:16 perms/models/base.py:54 #: perms/templates/perms/asset_permission_detail.html:98 #: perms/templates/perms/remote_app_permission_detail.html:90 -#: users/models/user.py:414 users/serializers/v1.py:143 +#: users/models/user.py:414 users/serializers/group.py:32 #: users/templates/users/user_detail.html:111 #: xpack/plugins/change_auth_plan/models.py:108 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:113 @@ -308,7 +308,7 @@ msgstr "远程应用" #: settings/templates/settings/security_setting.html:73 #: settings/templates/settings/terminal_setting.html:71 #: terminal/templates/terminal/terminal_update.html:45 -#: users/templates/users/_user.html:50 +#: users/templates/users/_user.html:51 #: users/templates/users/user_bulk_update.html:23 #: users/templates/users/user_detail.html:178 #: users/templates/users/user_group_create_update.html:31 @@ -352,7 +352,7 @@ msgstr "重置" #: terminal/templates/terminal/command_list.html:47 #: terminal/templates/terminal/session_list.html:52 #: terminal/templates/terminal/terminal_update.html:46 -#: users/templates/users/_user.html:51 +#: users/templates/users/_user.html:52 #: users/templates/users/forgot_password.html:42 #: users/templates/users/user_bulk_update.html:24 #: users/templates/users/user_list.html:57 @@ -410,7 +410,7 @@ msgstr "详情" #: assets/templates/assets/label_list.html:39 #: assets/templates/assets/system_user_detail.html:26 #: assets/templates/assets/system_user_list.html:29 -#: assets/templates/assets/system_user_list.html:81 audits/models.py:33 +#: assets/templates/assets/system_user_list.html:81 audits/models.py:34 #: perms/templates/perms/asset_permission_detail.html:30 #: perms/templates/perms/asset_permission_list.html:178 #: perms/templates/perms/remote_app_permission_detail.html:30 @@ -454,7 +454,7 @@ msgstr "更新" #: assets/templates/assets/domain_list.html:55 #: assets/templates/assets/label_list.html:40 #: assets/templates/assets/system_user_detail.html:30 -#: assets/templates/assets/system_user_list.html:82 audits/models.py:34 +#: assets/templates/assets/system_user_list.html:82 audits/models.py:35 #: authentication/templates/authentication/_access_key_modal.html:65 #: ops/templates/ops/task_list.html:69 #: perms/templates/perms/asset_permission_detail.html:34 @@ -510,7 +510,7 @@ msgstr "创建远程应用" #: assets/templates/assets/domain_gateway_list.html:73 #: assets/templates/assets/domain_list.html:29 #: assets/templates/assets/label_list.html:17 -#: assets/templates/assets/system_user_list.html:56 audits/models.py:38 +#: assets/templates/assets/system_user_list.html:56 audits/models.py:39 #: audits/templates/audits/operate_log_list.html:47 #: audits/templates/audits/operate_log_list.html:73 #: authentication/templates/authentication/_access_key_modal.html:34 @@ -634,7 +634,7 @@ msgid "Domain" msgstr "网域" #: assets/forms/asset.py:69 assets/forms/asset.py:103 assets/forms/asset.py:116 -#: assets/forms/asset.py:152 assets/models/node.py:421 +#: assets/forms/asset.py:152 assets/models/node.py:462 #: assets/serializers/system_user.py:36 #: assets/templates/assets/asset_create.html:42 #: perms/forms/asset_permission.py:87 perms/forms/asset_permission.py:94 @@ -679,9 +679,9 @@ msgstr "选择资产" msgid "Content should not be contain: {}" msgstr "内容不能包含: {}" -#: assets/forms/domain.py:55 +#: assets/forms/domain.py:55 assets/models/domain.py:67 msgid "Password should not contain special characters" -msgstr "不能包含特殊字符" +msgstr "密码不能包含特殊字符" #: assets/forms/domain.py:74 msgid "SSH gateway support proxy SSH,RDP,VNC" @@ -696,14 +696,14 @@ msgstr "SSH网关,支持代理SSH,RDP和VNC" #: assets/templates/assets/admin_user_list.html:45 #: assets/templates/assets/domain_gateway_list.html:71 #: assets/templates/assets/system_user_detail.html:62 -#: assets/templates/assets/system_user_list.html:48 audits/models.py:80 +#: assets/templates/assets/system_user_list.html:48 audits/models.py:81 #: audits/templates/audits/login_log_list.html:57 authentication/forms.py:13 #: authentication/templates/authentication/login.html:65 #: authentication/templates/authentication/new_login.html:92 #: ops/models/adhoc.py:189 perms/templates/perms/asset_permission_list.html:70 #: perms/templates/perms/asset_permission_user.html:55 #: perms/templates/perms/remote_app_permission_user.html:54 -#: settings/templates/settings/_ldap_list_users_modal.html:30 users/forms.py:13 +#: settings/templates/settings/_ldap_list_users_modal.html:31 users/forms.py:14 #: users/models/user.py:371 users/templates/users/_select_user_modal.html:14 #: users/templates/users/user_detail.html:67 #: users/templates/users/user_list.html:36 @@ -730,7 +730,7 @@ msgstr "密码或密钥密码" #: authentication/forms.py:15 #: authentication/templates/authentication/login.html:68 #: authentication/templates/authentication/new_login.html:95 -#: settings/forms.py:114 users/forms.py:15 users/forms.py:27 +#: settings/forms.py:114 users/forms.py:16 users/forms.py:42 #: users/templates/users/reset_password.html:53 #: users/templates/users/user_password_authentication.html:18 #: users/templates/users/user_password_update.html:44 @@ -1090,8 +1090,8 @@ msgstr "资产组" msgid "Default asset group" msgstr "默认资产组" -#: assets/models/label.py:15 audits/models.py:17 audits/models.py:37 -#: audits/models.py:50 audits/templates/audits/ftp_log_list.html:36 +#: assets/models/label.py:15 audits/models.py:18 audits/models.py:38 +#: audits/models.py:51 audits/templates/audits/ftp_log_list.html:36 #: audits/templates/audits/ftp_log_list.html:73 #: audits/templates/audits/operate_log_list.html:39 #: audits/templates/audits/operate_log_list.html:72 @@ -1110,9 +1110,10 @@ msgstr "默认资产组" #: terminal/models.py:156 terminal/templates/terminal/command_list.html:29 #: terminal/templates/terminal/command_list.html:65 #: terminal/templates/terminal/session_list.html:27 -#: terminal/templates/terminal/session_list.html:71 users/forms.py:319 +#: terminal/templates/terminal/session_list.html:71 users/forms.py:339 #: users/models/user.py:127 users/models/user.py:143 users/models/user.py:500 -#: users/serializers/v1.py:132 users/templates/users/user_group_detail.html:78 +#: users/serializers/group.py:21 +#: users/templates/users/user_group_detail.html:78 #: users/templates/users/user_group_list.html:36 users/views/user.py:250 #: xpack/plugins/orgs/forms.py:28 #: xpack/plugins/orgs/templates/orgs/org_detail.html:113 @@ -1120,7 +1121,7 @@ msgstr "默认资产组" msgid "User" msgstr "用户" -#: assets/models/label.py:19 assets/models/node.py:412 +#: assets/models/label.py:19 assets/models/node.py:453 #: assets/templates/assets/label_list.html:15 settings/models.py:30 msgid "Value" msgstr "值" @@ -1129,23 +1130,23 @@ msgstr "值" msgid "Category" msgstr "分类" -#: assets/models/node.py:163 +#: assets/models/node.py:164 msgid "New node" msgstr "新节点" -#: assets/models/node.py:324 +#: assets/models/node.py:325 msgid "ungrouped" msgstr "未分组" -#: assets/models/node.py:326 +#: assets/models/node.py:327 msgid "empty" msgstr "空" -#: assets/models/node.py:328 +#: assets/models/node.py:329 msgid "favorite" msgstr "收藏夹" -#: assets/models/node.py:411 +#: assets/models/node.py:452 msgid "Key" msgstr "键" @@ -1200,7 +1201,7 @@ msgid "Login mode" msgstr "登录模式" #: assets/models/user.py:166 assets/templates/assets/user_asset_list.html:79 -#: audits/models.py:20 audits/templates/audits/ftp_log_list.html:52 +#: audits/models.py:21 audits/templates/audits/ftp_log_list.html:52 #: audits/templates/audits/ftp_log_list.html:75 #: perms/forms/asset_permission.py:90 perms/forms/remote_app_permission.py:43 #: perms/models/asset_permission.py:82 perms/models/remote_app_permission.py:16 @@ -1265,7 +1266,7 @@ msgstr "组织名称" msgid "Backend" msgstr "后端" -#: assets/serializers/asset_user.py:67 users/forms.py:262 +#: assets/serializers/asset_user.py:67 users/forms.py:282 #: users/models/user.py:403 users/templates/users/first_login.html:42 #: users/templates/users/user_password_update.html:49 #: users/templates/users/user_profile.html:69 @@ -1436,9 +1437,10 @@ msgid "Asset list" msgstr "资产列表" #: assets/templates/assets/_asset_list_modal.html:33 -#: assets/templates/assets/_node_tree.html:40 +#: assets/templates/assets/_node_tree.html:39 #: ops/templates/ops/command_execution_create.html:70 #: ops/templates/ops/command_execution_create.html:127 +#: settings/templates/settings/_ldap_list_users_modal.html:41 #: users/templates/users/_granted_assets.html:7 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:66 msgid "Loading" @@ -1481,7 +1483,7 @@ msgstr "获取认证信息错误" #: assets/templates/assets/_user_asset_detail_modal.html:23 #: authentication/templates/authentication/_access_key_modal.html:142 #: authentication/templates/authentication/_mfa_confirm_modal.html:53 -#: settings/templates/settings/_ldap_list_users_modal.html:92 +#: settings/templates/settings/_ldap_list_users_modal.html:171 #: templates/_modal.html:22 msgid "Close" msgstr "关闭" @@ -1531,31 +1533,31 @@ msgstr "SSH端口" msgid "If use nat, set the ssh real port" msgstr "如果使用了nat端口映射,请设置为ssh真实监听的端口" -#: assets/templates/assets/_node_tree.html:50 +#: assets/templates/assets/_node_tree.html:49 msgid "Add node" msgstr "新建节点" -#: assets/templates/assets/_node_tree.html:51 +#: assets/templates/assets/_node_tree.html:50 msgid "Rename node" msgstr "重命名节点" -#: assets/templates/assets/_node_tree.html:52 +#: assets/templates/assets/_node_tree.html:51 msgid "Delete node" msgstr "删除节点" -#: assets/templates/assets/_node_tree.html:166 +#: assets/templates/assets/_node_tree.html:165 msgid "Create node failed" msgstr "创建节点失败" -#: assets/templates/assets/_node_tree.html:178 +#: assets/templates/assets/_node_tree.html:177 msgid "Have child node, cancel" msgstr "存在子节点,不能删除" -#: assets/templates/assets/_node_tree.html:180 +#: assets/templates/assets/_node_tree.html:179 msgid "Have assets, cancel" msgstr "存在资产,不能删除" -#: assets/templates/assets/_node_tree.html:255 +#: assets/templates/assets/_node_tree.html:254 msgid "Rename success" msgstr "重命名成功" @@ -1697,7 +1699,7 @@ msgstr "导出" #: assets/templates/assets/admin_user_list.html:21 #: assets/templates/assets/asset_list.html:73 #: assets/templates/assets/system_user_list.html:24 -#: settings/templates/settings/_ldap_list_users_modal.html:93 +#: settings/templates/settings/_ldap_list_users_modal.html:172 #: users/templates/users/user_group_list.html:15 #: users/templates/users/user_list.html:15 #: xpack/plugins/license/templates/license/license_detail.html:110 @@ -1882,16 +1884,16 @@ msgstr "删除选择资产" msgid "Cancel" msgstr "取消" -#: assets/templates/assets/asset_list.html:434 +#: assets/templates/assets/asset_list.html:432 msgid "Asset Deleted." msgstr "已被删除" -#: assets/templates/assets/asset_list.html:435 -#: assets/templates/assets/asset_list.html:439 +#: assets/templates/assets/asset_list.html:433 +#: assets/templates/assets/asset_list.html:441 msgid "Asset Delete" msgstr "删除" -#: assets/templates/assets/asset_list.html:438 +#: assets/templates/assets/asset_list.html:440 msgid "Asset Deleting failed." msgstr "删除失败" @@ -1983,10 +1985,6 @@ msgstr "测试连接" msgid "Can be connected" msgstr "可连接" -#: assets/templates/assets/domain_gateway_list.html:142 -msgid "The connection fails" -msgstr "连接失败" - #: assets/templates/assets/domain_list.html:9 msgid "" "The domain function is added to address the fact that some environments " @@ -2180,7 +2178,7 @@ msgstr "资产管理" msgid "System user asset" msgstr "系统用户资产" -#: audits/models.py:18 audits/models.py:41 audits/models.py:52 +#: audits/models.py:19 audits/models.py:42 audits/models.py:53 #: audits/templates/audits/ftp_log_list.html:76 #: audits/templates/audits/operate_log_list.html:76 #: audits/templates/audits/password_change_log_list.html:58 @@ -2190,16 +2188,16 @@ msgstr "系统用户资产" msgid "Remote addr" msgstr "远端地址" -#: audits/models.py:21 audits/templates/audits/ftp_log_list.html:77 +#: audits/models.py:22 audits/templates/audits/ftp_log_list.html:77 msgid "Operate" msgstr "操作" -#: audits/models.py:22 audits/templates/audits/ftp_log_list.html:59 +#: audits/models.py:23 audits/templates/audits/ftp_log_list.html:59 #: audits/templates/audits/ftp_log_list.html:78 msgid "Filename" msgstr "文件名" -#: audits/models.py:23 audits/models.py:76 +#: audits/models.py:24 audits/models.py:77 #: audits/templates/audits/ftp_log_list.html:79 #: ops/templates/ops/command_execution_list.html:68 #: ops/templates/ops/task_list.html:15 @@ -2209,67 +2207,67 @@ msgstr "文件名" msgid "Success" msgstr "成功" -#: audits/models.py:32 +#: audits/models.py:33 #: authentication/templates/authentication/_access_key_modal.html:22 #: xpack/plugins/vault/templates/vault/vault.html:46 msgid "Create" msgstr "创建" -#: audits/models.py:39 audits/templates/audits/operate_log_list.html:55 +#: audits/models.py:40 audits/templates/audits/operate_log_list.html:55 #: audits/templates/audits/operate_log_list.html:74 msgid "Resource Type" msgstr "资源类型" -#: audits/models.py:40 audits/templates/audits/operate_log_list.html:75 +#: audits/models.py:41 audits/templates/audits/operate_log_list.html:75 msgid "Resource" msgstr "资源" -#: audits/models.py:51 audits/templates/audits/password_change_log_list.html:57 +#: audits/models.py:52 audits/templates/audits/password_change_log_list.html:57 msgid "Change by" msgstr "修改者" -#: audits/models.py:70 users/templates/users/user_detail.html:98 +#: audits/models.py:71 users/templates/users/user_detail.html:98 msgid "Disabled" msgstr "禁用" -#: audits/models.py:71 settings/models.py:33 +#: audits/models.py:72 settings/models.py:33 #: users/templates/users/user_detail.html:96 msgid "Enabled" msgstr "启用" -#: audits/models.py:72 +#: audits/models.py:73 msgid "-" msgstr "" -#: audits/models.py:77 xpack/plugins/cloud/models.py:264 +#: audits/models.py:78 xpack/plugins/cloud/models.py:264 #: xpack/plugins/cloud/models.py:287 msgid "Failed" msgstr "失败" -#: audits/models.py:81 +#: audits/models.py:82 msgid "Login type" msgstr "登录方式" -#: audits/models.py:82 +#: audits/models.py:83 msgid "Login ip" msgstr "登录IP" -#: audits/models.py:83 +#: audits/models.py:84 msgid "Login city" msgstr "登录城市" -#: audits/models.py:84 +#: audits/models.py:85 msgid "User agent" msgstr "Agent" -#: audits/models.py:85 audits/templates/audits/login_log_list.html:62 +#: audits/models.py:86 audits/templates/audits/login_log_list.html:62 #: authentication/templates/authentication/_mfa_confirm_modal.html:14 -#: users/forms.py:174 users/models/user.py:395 +#: users/forms.py:194 users/models/user.py:395 #: users/templates/users/first_login.html:45 msgid "MFA" msgstr "MFA" -#: audits/models.py:86 audits/templates/audits/login_log_list.html:63 +#: audits/models.py:87 audits/templates/audits/login_log_list.html:63 #: xpack/plugins/change_auth_plan/models.py:416 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:15 #: xpack/plugins/cloud/models.py:278 @@ -2277,14 +2275,14 @@ msgstr "MFA" msgid "Reason" msgstr "原因" -#: audits/models.py:87 audits/templates/audits/login_log_list.html:64 +#: audits/models.py:88 audits/templates/audits/login_log_list.html:64 #: xpack/plugins/cloud/models.py:275 xpack/plugins/cloud/models.py:310 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:70 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:65 msgid "Status" msgstr "状态" -#: audits/models.py:88 +#: audits/models.py:89 msgid "Date login" msgstr "登录日期" @@ -2487,7 +2485,7 @@ msgid "" "after {} minutes)" msgstr "账号已被锁定(请联系管理员解锁 或 {}分钟后重试)" -#: authentication/forms.py:66 users/forms.py:21 +#: authentication/forms.py:66 users/forms.py:22 msgid "MFA code" msgstr "MFA 验证码" @@ -2826,23 +2824,23 @@ msgstr "Become" msgid "Create by" msgstr "创建者" -#: ops/models/adhoc.py:251 +#: ops/models/adhoc.py:252 msgid "{} Start task: {}" msgstr "{} 任务开始: {}" -#: ops/models/adhoc.py:263 +#: ops/models/adhoc.py:264 msgid "{} Task finish" msgstr "{} 任务结束" -#: ops/models/adhoc.py:355 +#: ops/models/adhoc.py:356 msgid "Start time" msgstr "开始时间" -#: ops/models/adhoc.py:356 +#: ops/models/adhoc.py:357 msgid "End time" msgstr "完成时间" -#: ops/models/adhoc.py:357 ops/templates/ops/adhoc_history.html:57 +#: ops/models/adhoc.py:358 ops/templates/ops/adhoc_history.html:57 #: ops/templates/ops/task_history.html:63 ops/templates/ops/task_list.html:17 #: xpack/plugins/change_auth_plan/models.py:252 #: xpack/plugins/change_auth_plan/models.py:422 @@ -2852,23 +2850,23 @@ msgstr "完成时间" msgid "Time" msgstr "时间" -#: ops/models/adhoc.py:358 ops/templates/ops/adhoc_detail.html:106 +#: ops/models/adhoc.py:359 ops/templates/ops/adhoc_detail.html:106 #: ops/templates/ops/adhoc_history.html:55 #: ops/templates/ops/adhoc_history_detail.html:69 #: ops/templates/ops/task_detail.html:84 ops/templates/ops/task_history.html:61 msgid "Is finished" msgstr "是否完成" -#: ops/models/adhoc.py:359 ops/templates/ops/adhoc_history.html:56 +#: ops/models/adhoc.py:360 ops/templates/ops/adhoc_history.html:56 #: ops/templates/ops/task_history.html:62 msgid "Is success" msgstr "是否成功" -#: ops/models/adhoc.py:360 +#: ops/models/adhoc.py:361 msgid "Adhoc raw result" msgstr "结果" -#: ops/models/adhoc.py:361 +#: ops/models/adhoc.py:362 msgid "Adhoc result summary" msgstr "汇总" @@ -3135,7 +3133,7 @@ msgstr "提示:RDP 协议不支持单独控制上传或下载文件" #: perms/templates/perms/asset_permission_list.html:71 #: perms/templates/perms/asset_permission_list.html:118 #: perms/templates/perms/remote_app_permission_list.html:16 -#: templates/_nav.html:21 users/forms.py:293 users/models/group.py:26 +#: templates/_nav.html:21 users/forms.py:313 users/models/group.py:26 #: users/models/user.py:379 users/templates/users/_select_user_modal.html:16 #: users/templates/users/user_detail.html:218 #: users/templates/users/user_list.html:38 @@ -3395,33 +3393,41 @@ msgstr "远程应用授权用户列表" msgid "RemoteApp permission RemoteApp list" msgstr "远程应用授权远程应用列表" -#: settings/api.py:28 +#: settings/api.py:37 msgid "Test mail sent to {}, please check" msgstr "邮件已经发送{}, 请检查" -#: settings/api.py:67 +#: settings/api.py:76 msgid "Test ldap success" msgstr "连接LDAP成功" -#: settings/api.py:104 +#: settings/api.py:107 +msgid "LDAP attr map not valid" +msgstr "LDAP 属性映射无效" + +#: settings/api.py:116 msgid "Match {} s users" msgstr "匹配 {} 个用户" -#: settings/api.py:163 -msgid "succeed: {} failed: {} total: {}" -msgstr "成功:{} 失败:{} 总数:{}" +#: settings/api.py:224 +msgid "Get ldap users is None" +msgstr "获取 LDAP 用户为 None" -#: settings/api.py:185 settings/api.py:221 +#: settings/api.py:231 +msgid "Imported {} users successfully" +msgstr "导入 {} 个用户成功" + +#: settings/api.py:262 settings/api.py:298 msgid "" "Error: Account invalid (Please make sure the information such as Access key " "or Secret key is correct)" msgstr "错误:账户无效 (请确保 Access key 或 Secret key 等信息正确)" -#: settings/api.py:191 settings/api.py:227 +#: settings/api.py:268 settings/api.py:304 msgid "Create succeed" msgstr "创建成功" -#: settings/api.py:209 settings/api.py:247 +#: settings/api.py:286 settings/api.py:324 #: settings/templates/settings/terminal_setting.html:154 msgid "Delete succeed" msgstr "删除成功" @@ -3741,23 +3747,32 @@ msgstr "LDAP 用户列表" msgid "Please submit the LDAP configuration before import" msgstr "请先提交LDAP配置再进行导入" -#: settings/templates/settings/_ldap_list_users_modal.html:32 +#: settings/templates/settings/_ldap_list_users_modal.html:26 +msgid "Refresh cache" +msgstr "刷新缓存" + +#: settings/templates/settings/_ldap_list_users_modal.html:33 #: users/models/user.py:375 users/templates/users/user_detail.html:71 #: users/templates/users/user_profile.html:59 msgid "Email" msgstr "邮件" -#: settings/templates/settings/_ldap_list_users_modal.html:33 +#: settings/templates/settings/_ldap_list_users_modal.html:34 msgid "Existing" msgstr "已存在" +#: settings/templates/settings/_ldap_list_users_modal.html:144 +msgid "" +"User is not currently selected, please check the user you want to import" +msgstr "当前无勾选用户,请勾选你想要导入的用户" + #: settings/templates/settings/basic_setting.html:15 #: settings/templates/settings/email_content_setting.html:15 #: settings/templates/settings/email_setting.html:15 #: settings/templates/settings/ldap_setting.html:15 #: settings/templates/settings/security_setting.html:15 #: settings/templates/settings/terminal_setting.html:16 -#: settings/templates/settings/terminal_setting.html:49 settings/views.py:20 +#: settings/templates/settings/terminal_setting.html:49 settings/views.py:21 msgid "Basic setting" msgstr "基本设置" @@ -3766,7 +3781,7 @@ msgstr "基本设置" #: settings/templates/settings/email_setting.html:18 #: settings/templates/settings/ldap_setting.html:18 #: settings/templates/settings/security_setting.html:18 -#: settings/templates/settings/terminal_setting.html:20 settings/views.py:47 +#: settings/templates/settings/terminal_setting.html:20 settings/views.py:48 msgid "Email setting" msgstr "邮件设置" @@ -3775,7 +3790,7 @@ msgstr "邮件设置" #: settings/templates/settings/email_setting.html:21 #: settings/templates/settings/ldap_setting.html:21 #: settings/templates/settings/security_setting.html:21 -#: settings/templates/settings/terminal_setting.html:23 settings/views.py:186 +#: settings/templates/settings/terminal_setting.html:23 settings/views.py:188 msgid "Email content setting" msgstr "邮件内容设置" @@ -3784,7 +3799,7 @@ msgstr "邮件内容设置" #: settings/templates/settings/email_setting.html:24 #: settings/templates/settings/ldap_setting.html:24 #: settings/templates/settings/security_setting.html:24 -#: settings/templates/settings/terminal_setting.html:27 settings/views.py:74 +#: settings/templates/settings/terminal_setting.html:27 settings/views.py:75 msgid "LDAP setting" msgstr "LDAP设置" @@ -3793,7 +3808,7 @@ msgstr "LDAP设置" #: settings/templates/settings/email_setting.html:27 #: settings/templates/settings/ldap_setting.html:27 #: settings/templates/settings/security_setting.html:27 -#: settings/templates/settings/terminal_setting.html:31 settings/views.py:104 +#: settings/templates/settings/terminal_setting.html:31 settings/views.py:106 msgid "Terminal setting" msgstr "终端设置" @@ -3803,7 +3818,7 @@ msgstr "终端设置" #: settings/templates/settings/ldap_setting.html:30 #: settings/templates/settings/security_setting.html:30 #: settings/templates/settings/security_setting.html:45 -#: settings/templates/settings/terminal_setting.html:34 settings/views.py:159 +#: settings/templates/settings/terminal_setting.html:34 settings/views.py:161 msgid "Security setting" msgstr "安全设置" @@ -3823,15 +3838,10 @@ msgstr "文档类型" msgid "Create User setting" msgstr "创建用户设置" -#: settings/templates/settings/ldap_setting.html:68 +#: settings/templates/settings/ldap_setting.html:66 msgid "Bulk import" msgstr "一键导入" -#: settings/templates/settings/ldap_setting.html:116 -msgid "" -"User is not currently selected, please check the user you want to import" -msgstr "当前无勾选用户,请勾选你想要导入的用户" - #: settings/templates/settings/replay_storage_create.html:66 msgid "Bucket" msgstr "桶名称" @@ -3936,30 +3946,26 @@ msgstr "删除失败" msgid "Are you sure about deleting it?" msgstr "您确定删除吗?" -#: settings/utils.py:98 +#: settings/utils/ldap.py:128 msgid "Search no entry matched in ou {}" msgstr "在ou:{}中没有匹配条目" -#: settings/utils.py:172 -msgid "The user source is not LDAP" -msgstr "用户来源不是LDAP" - -#: settings/views.py:19 settings/views.py:46 settings/views.py:73 -#: settings/views.py:103 settings/views.py:131 settings/views.py:144 -#: settings/views.py:158 settings/views.py:185 templates/_nav.html:170 +#: settings/views.py:20 settings/views.py:47 settings/views.py:74 +#: settings/views.py:105 settings/views.py:133 settings/views.py:146 +#: settings/views.py:160 settings/views.py:187 templates/_nav.html:170 msgid "Settings" msgstr "系统设置" -#: settings/views.py:30 settings/views.py:57 settings/views.py:84 -#: settings/views.py:116 settings/views.py:169 settings/views.py:196 +#: settings/views.py:31 settings/views.py:58 settings/views.py:85 +#: settings/views.py:118 settings/views.py:171 settings/views.py:198 msgid "Update setting successfully" msgstr "更新设置成功" -#: settings/views.py:132 +#: settings/views.py:134 msgid "Create replay storage" msgstr "创建录像存储" -#: settings/views.py:145 +#: settings/views.py:147 msgid "Create command storage" msgstr "创建命令存储" @@ -3976,8 +3982,8 @@ msgid "Commercial support" msgstr "商业支持" #: templates/_header_bar.html:70 templates/_nav.html:30 -#: templates/_nav_user.html:32 users/forms.py:153 -#: users/templates/users/_user.html:43 +#: templates/_nav_user.html:32 users/forms.py:173 +#: users/templates/users/_user.html:44 #: users/templates/users/first_login.html:39 #: users/templates/users/user_password_update.html:40 #: users/templates/users/user_profile.html:17 @@ -4541,7 +4547,7 @@ msgstr "你可以使用ssh客户端工具连接终端" msgid "Could not reset self otp, use profile reset instead" msgstr "不能再该页面重置MFA, 请去个人信息页面重置" -#: users/forms.py:32 users/models/user.py:383 +#: users/forms.py:47 users/models/user.py:383 #: users/templates/users/_select_user_modal.html:15 #: users/templates/users/user_detail.html:87 #: users/templates/users/user_list.html:37 @@ -4549,44 +4555,51 @@ msgstr "不能再该页面重置MFA, 请去个人信息页面重置" msgid "Role" msgstr "角色" -#: users/forms.py:35 users/forms.py:232 +#: users/forms.py:51 users/models/user.py:418 +#: users/templates/users/user_detail.html:103 +#: users/templates/users/user_list.html:39 +#: users/templates/users/user_profile.html:102 +msgid "Source" +msgstr "用户来源" + +#: users/forms.py:54 users/forms.py:252 #: users/templates/users/user_update.html:30 msgid "ssh public key" msgstr "ssh公钥" -#: users/forms.py:36 users/forms.py:233 +#: users/forms.py:55 users/forms.py:253 msgid "ssh-rsa AAAA..." msgstr "" -#: users/forms.py:37 +#: users/forms.py:56 msgid "Paste user id_rsa.pub here." msgstr "复制用户公钥到这里" -#: users/forms.py:51 users/templates/users/user_detail.html:226 +#: users/forms.py:71 users/templates/users/user_detail.html:226 msgid "Join user groups" msgstr "添加到用户组" -#: users/forms.py:86 users/forms.py:247 +#: users/forms.py:106 users/forms.py:267 msgid "Public key should not be the same as your old one." msgstr "不能和原来的密钥相同" -#: users/forms.py:90 users/forms.py:251 users/serializers/v1.py:116 +#: users/forms.py:110 users/forms.py:271 users/serializers/user.py:110 msgid "Not a valid ssh public key" msgstr "ssh密钥不合法" -#: users/forms.py:103 users/views/login.py:114 users/views/user.py:287 +#: users/forms.py:123 users/views/login.py:114 users/views/user.py:287 msgid "* Your password does not meet the requirements" msgstr "* 您的密码不符合要求" -#: users/forms.py:124 +#: users/forms.py:144 msgid "Reset link will be generated and sent to the user" msgstr "生成重置密码链接,通过邮件发送给用户" -#: users/forms.py:125 +#: users/forms.py:145 msgid "Set password" msgstr "设置密码" -#: users/forms.py:132 xpack/plugins/change_auth_plan/models.py:88 +#: users/forms.py:152 xpack/plugins/change_auth_plan/models.py:88 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:51 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:69 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:57 @@ -4594,7 +4607,7 @@ msgstr "设置密码" msgid "Password strategy" msgstr "密码策略" -#: users/forms.py:159 +#: users/forms.py:179 msgid "" "When enabled, you will enter the MFA binding process the next time you log " "in. you can also directly bind in \"personal information -> quick " @@ -4603,11 +4616,11 @@ msgstr "" "启用之后您将会在下次登录时进入MFA绑定流程;您也可以在(个人信息->快速修改->更" "改MFA设置)中直接绑定!" -#: users/forms.py:169 +#: users/forms.py:189 msgid "* Enable MFA authentication to make the account more secure." msgstr "* 启用MFA认证,使账号更加安全。" -#: users/forms.py:179 +#: users/forms.py:199 msgid "" "In order to protect you and your company, please keep your account, password " "and key sensitive information properly. (for example: setting complex " @@ -4616,41 +4629,41 @@ msgstr "" "为了保护您和公司的安全,请妥善保管您的账户、密码和密钥等重要敏感信息;(如:" "设置复杂密码,启用MFA认证)" -#: users/forms.py:186 users/templates/users/first_login.html:48 +#: users/forms.py:206 users/templates/users/first_login.html:48 #: users/templates/users/first_login.html:110 #: users/templates/users/first_login.html:139 msgid "Finish" msgstr "完成" -#: users/forms.py:192 +#: users/forms.py:212 msgid "Old password" msgstr "原来密码" -#: users/forms.py:197 +#: users/forms.py:217 msgid "New password" msgstr "新密码" -#: users/forms.py:202 +#: users/forms.py:222 msgid "Confirm password" msgstr "确认密码" -#: users/forms.py:212 +#: users/forms.py:232 msgid "Old password error" msgstr "原来密码错误" -#: users/forms.py:220 +#: users/forms.py:240 msgid "Password does not match" msgstr "密码不一致" -#: users/forms.py:230 +#: users/forms.py:250 msgid "Automatically configure and download the SSH key" msgstr "自动配置并下载SSH密钥" -#: users/forms.py:234 +#: users/forms.py:254 msgid "Paste your id_rsa.pub here." msgstr "复制你的公钥到这里" -#: users/forms.py:268 users/forms.py:273 users/forms.py:323 +#: users/forms.py:288 users/forms.py:293 users/forms.py:343 #: xpack/plugins/orgs/forms.py:18 msgid "Select users" msgstr "选择用户" @@ -4693,12 +4706,6 @@ msgstr "头像" msgid "Wechat" msgstr "微信" -#: users/models/user.py:418 users/templates/users/user_detail.html:103 -#: users/templates/users/user_list.html:39 -#: users/templates/users/user_profile.html:102 -msgid "Source" -msgstr "用户来源" - #: users/models/user.py:422 msgid "Date password last updated" msgstr "最后更新密码日期" @@ -4707,46 +4714,46 @@ msgstr "最后更新密码日期" msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" -#: users/serializers/v1.py:45 +#: users/serializers/group.py:46 +msgid "Auditors cannot be join in the user group" +msgstr "审计员不能被加入到用户组" + +#: users/serializers/user.py:40 msgid "Groups name" msgstr "用户组名" -#: users/serializers/v1.py:46 +#: users/serializers/user.py:41 msgid "Source name" msgstr "用户来源名" -#: users/serializers/v1.py:47 +#: users/serializers/user.py:42 msgid "Is first login" msgstr "首次登录" -#: users/serializers/v1.py:48 +#: users/serializers/user.py:43 msgid "Role name" msgstr "角色名" -#: users/serializers/v1.py:49 +#: users/serializers/user.py:44 msgid "Is valid" msgstr "账户是否有效" -#: users/serializers/v1.py:50 +#: users/serializers/user.py:45 msgid "Is expired" msgstr " 是否过期" -#: users/serializers/v1.py:51 +#: users/serializers/user.py:46 msgid "Avatar url" msgstr "头像路径" -#: users/serializers/v1.py:72 +#: users/serializers/user.py:66 msgid "Role limit to {}" msgstr "角色只能为 {}" -#: users/serializers/v1.py:84 +#: users/serializers/user.py:78 msgid "Password does not match security rules" msgstr "密码不满足安全规则" -#: users/serializers/v1.py:157 -msgid "Auditors cannot be join in the user group" -msgstr "审计员不能被加入到用户组" - #: users/serializers_v2/user.py:36 msgid "name not unique" msgstr "名称重复" @@ -4779,7 +4786,7 @@ msgstr "选择用户" msgid "Asset num" msgstr "资产数量" -#: users/templates/users/_user.html:26 +#: users/templates/users/_user.html:27 msgid "Security and Role" msgstr "角色安全" @@ -6206,6 +6213,14 @@ msgstr "密码匣子" msgid "vault create" msgstr "创建" +#, fuzzy +#~| msgid "Password should not contain special characters" +#~ msgid "Password has special char" +#~ msgstr "不能包含特殊字符" + +#~ msgid "The connection fails" +#~ msgstr "连接失败" + #~ msgid "Recipient" #~ msgstr "收件人" @@ -6331,25 +6346,6 @@ msgstr "创建" #~ msgid "Sync User" #~ msgstr "同步用户" -#~ msgid "Have user but attr mapping error" -#~ msgstr "有用户但attr映射错误" - -#~ msgid "" -#~ "Import {} users successfully; import {} users failed, the database " -#~ "already exists with the same name" -#~ msgstr "导入 {} 个用户成功; 导入 {} 这些用户失败,数据库已经存在同名的用户" - -#~ msgid "" -#~ "Import {} users successfully; import {} users failed, the database " -#~ "already exists with the same name; import {}users failed, " -#~ "Because’TypeError' object has no attribute 'keys'" -#~ msgstr "" -#~ "导入 {} 个用户成功; 导入 {} 这些用户失败,数据库已经存在同名的用户; 导入 " -#~ "{} 这些用户失败,因为对象没有属性'keys'" - -#~ msgid "Import {} users successfully" -#~ msgstr "导入 {} 个用户成功" - #~ msgid "" #~ "Import {} users successfully;import {} users failed, Because’TypeError' " #~ "object has no attribute 'keys'" diff --git a/apps/perms/signals_handler.py b/apps/perms/signals_handler.py index eecd101e8..4a7fe389c 100644 --- a/apps/perms/signals_handler.py +++ b/apps/perms/signals_handler.py @@ -11,13 +11,6 @@ from .utils.asset_permission import AssetPermissionUtilV2 logger = get_logger(__file__) -permission_m2m_senders = ( - AssetPermission.nodes.through, - AssetPermission.assets.through, - AssetPermission.users.through, - AssetPermission.user_groups.through, -) - @receiver([post_save, post_delete], sender=AssetPermission) @on_transaction_commit diff --git a/apps/settings/api.py b/apps/settings/api.py index 22a295b68..1fce9a44d 100644 --- a/apps/settings/api.py +++ b/apps/settings/api.py @@ -13,17 +13,26 @@ from django.core.mail import send_mail from django.utils.translation import ugettext_lazy as _ from .models import Setting -from .utils import LDAPUtil +from .utils import ( + LDAPServerUtil, LDAPCacheUtil, LDAPImportUtil, LDAPSyncUtil, + LDAP_USE_CACHE_FLAGS + +) +from .tasks import sync_ldap_user_task from common.permissions import IsOrgAdmin, IsSuperUser from common.utils import get_logger -from .serializers import MailTestSerializer, LDAPTestSerializer, LDAPUserSerializer +from .serializers import ( + MailTestSerializer, LDAPTestSerializer, LDAPUserSerializer, + PublicSettingSerializer, +) +from users.models import User logger = get_logger(__file__) class MailTestingAPI(APIView): - permission_classes = (IsOrgAdmin,) + permission_classes = (IsSuperUser,) serializer_class = MailTestSerializer success_message = _("Test mail sent to {}, please check") @@ -62,70 +71,83 @@ class MailTestingAPI(APIView): class LDAPTestingAPI(APIView): - permission_classes = (IsOrgAdmin,) + permission_classes = (IsSuperUser,) serializer_class = LDAPTestSerializer success_message = _("Test ldap success") @staticmethod - def get_ldap_util(serializer): - host = serializer.validated_data["AUTH_LDAP_SERVER_URI"] + def get_ldap_config(serializer): + server_uri = serializer.validated_data["AUTH_LDAP_SERVER_URI"] bind_dn = serializer.validated_data["AUTH_LDAP_BIND_DN"] password = serializer.validated_data["AUTH_LDAP_BIND_PASSWORD"] use_ssl = serializer.validated_data.get("AUTH_LDAP_START_TLS", False) search_ougroup = serializer.validated_data["AUTH_LDAP_SEARCH_OU"] search_filter = serializer.validated_data["AUTH_LDAP_SEARCH_FILTER"] attr_map = serializer.validated_data["AUTH_LDAP_USER_ATTR_MAP"] - try: - attr_map = json.loads(attr_map) - except json.JSONDecodeError: - return Response({"error": "AUTH_LDAP_USER_ATTR_MAP not valid"}, status=401) - - util = LDAPUtil( - use_settings_config=False, server_uri=host, bind_dn=bind_dn, - password=password, use_ssl=use_ssl, - search_ougroup=search_ougroup, search_filter=search_filter, - attr_map=attr_map - ) - return util + config = { + 'server_uri': server_uri, + 'bind_dn': bind_dn, + 'password': password, + 'use_ssl': use_ssl, + 'search_ougroup': search_ougroup, + 'search_filter': search_filter, + 'attr_map': json.loads(attr_map), + } + return config def post(self, request): serializer = self.serializer_class(data=request.data) if not serializer.is_valid(): return Response({"error": str(serializer.errors)}, status=401) - util = self.get_ldap_util(serializer) - + attr_map = serializer.validated_data["AUTH_LDAP_USER_ATTR_MAP"] try: - users = util.search_user_items() + json.loads(attr_map) + except json.JSONDecodeError: + return Response({"error": _("LDAP attr map not valid")}, status=401) + + config = self.get_ldap_config(serializer) + util = LDAPServerUtil(config=config) + try: + users = util.search() except Exception as e: return Response({"error": str(e)}, status=401) - if len(users) > 0: - return Response({"msg": _("Match {} s users").format(len(users))}) - else: - return Response({"error": "Have user but attr mapping error"}, status=401) + return Response({"msg": _("Match {} s users").format(len(users))}) class LDAPUserListApi(generics.ListAPIView): - permission_classes = (IsOrgAdmin,) + permission_classes = (IsSuperUser,) serializer_class = LDAPUserSerializer + def get_queryset_from_cache(self): + search_value = self.request.query_params.get('search') + users = LDAPCacheUtil().search(search_value=search_value) + return users + + def get_queryset_from_server(self): + search_value = self.request.query_params.get('search') + users = LDAPServerUtil().search(search_value=search_value) + return users + def get_queryset(self): if hasattr(self, 'swagger_fake_view'): return [] - q = self.request.query_params.get('search') - try: - util = LDAPUtil() - extra_filter = util.construct_extra_filter(util.SEARCH_FIELD_ALL, q) - users = util.search_user_items(extra_filter) - except Exception as e: - users = [] - logger.error(e) - # 前端data_table会根据row.id对table.selected值进行操作 - for user in users: - user['id'] = user['username'] + cache_police = self.request.query_params.get('cache_police', True) + if cache_police in LDAP_USE_CACHE_FLAGS: + users = self.get_queryset_from_cache() + else: + users = self.get_queryset_from_server() return users + @staticmethod + def processing_queryset(queryset): + db_username_list = User.objects.all().values_list('username', flat=True) + for q in queryset: + q['id'] = q['username'] + q['existing'] = q['username'] in db_username_list + return queryset + def sort_queryset(self, queryset): order_by = self.request.query_params.get('order') if not order_by: @@ -138,32 +160,89 @@ class LDAPUserListApi(generics.ListAPIView): queryset = sorted(queryset, key=lambda x: x[order_by], reverse=reverse) return queryset - def list(self, request, *args, **kwargs): - queryset = self.get_queryset() + def filter_queryset(self, queryset): + if queryset is None: + return queryset + queryset = self.processing_queryset(queryset) queryset = self.sort_queryset(queryset) - page = self.paginate_queryset(queryset) - if page is not None: - return self.get_paginated_response(page) - return Response(queryset) + return queryset + + def list(self, request, *args, **kwargs): + cache_police = self.request.query_params.get('cache_police', True) + # 不是用缓存 + if cache_police not in LDAP_USE_CACHE_FLAGS: + return super().list(request, *args, **kwargs) + + try: + queryset = self.get_queryset() + except Exception as e: + data = {'error': str(e)} + return Response(data=data, status=400) + + # 缓存有数据 + if queryset is not None: + return super().list(request, *args, **kwargs) + + sync_util = LDAPSyncUtil() + # 还没有同步任务 + if sync_util.task_no_start: + # 任务外部设置 task running 状态 + sync_util.set_task_status(sync_util.TASK_STATUS_IS_RUNNING) + task = sync_ldap_user_task.delay() + data = {'msg': 'Cache no data, sync task {} started.'.format(task.id)} + return Response(data=data, status=409) + # 同步任务正在执行 + if sync_util.task_is_running: + data = {'msg': 'synchronization is running.'} + return Response(data=data, status=409) + # 同步任务执行结束 + if sync_util.task_is_over: + msg = sync_util.get_task_error_msg() + data = {'error': 'Synchronization task report error: {}'.format(msg)} + return Response(data=data, status=400) + + return super().list(request, *args, **kwargs) -class LDAPUserSyncAPI(APIView): - permission_classes = (IsOrgAdmin,) +class LDAPUserImportAPI(APIView): + permission_classes = (IsSuperUser,) + + def get_ldap_users(self): + username_list = self.request.data.get('username_list', []) + cache_police = self.request.query_params.get('cache_police', True) + if cache_police in LDAP_USE_CACHE_FLAGS: + users = LDAPCacheUtil().search(search_users=username_list) + else: + users = LDAPServerUtil().search(search_users=username_list) + return users def post(self, request): - username_list = request.data.get('username_list', []) - - util = LDAPUtil() try: - result = util.sync_users(username_list) + users = self.get_ldap_users() except Exception as e: - logger.error(e, exc_info=True) return Response({'error': str(e)}, status=401) - else: - msg = _("succeed: {} failed: {} total: {}").format( - result['succeed'], result['failed'], result['total'] - ) - return Response({'msg': msg}) + + if users is None: + return Response({'msg': _('Get ldap users is None')}, status=401) + + errors = LDAPImportUtil().perform_import(users) + if errors: + return Response({'errors': errors}, status=401) + + count = users if users is None else len(users) + return Response({'msg': _('Imported {} users successfully').format(count)}) + + +class LDAPCacheRefreshAPI(generics.RetrieveAPIView): + permission_classes = (IsSuperUser,) + + def retrieve(self, request, *args, **kwargs): + try: + LDAPSyncUtil().clear_cache() + except Exception as e: + logger.error(str(e)) + return Response(data={'msg': str(e)}, status=400) + return Response(data={'msg': 'success'}) class ReplayStorageCreateAPI(APIView): @@ -245,3 +324,19 @@ class CommandStorageDeleteAPI(APIView): storage_name = str(request.data.get('name')) Setting.delete_storage('TERMINAL_COMMAND_STORAGE', storage_name) return Response({"msg": _('Delete succeed')}, status=200) + + +class PublicSettingApi(generics.RetrieveAPIView): + permission_classes = () + serializer_class = PublicSettingSerializer + + def get_object(self): + c = settings.CONFIG + instance = { + "data": { + "WINDOWS_SKIP_ALL_MANUAL_PASSWORD": c.WINDOWS_SKIP_ALL_MANUAL_PASSWORD + } + } + return instance + + diff --git a/apps/settings/serializers.py b/apps/settings/serializers.py index eb8a61679..1717634f4 100644 --- a/apps/settings/serializers.py +++ b/apps/settings/serializers.py @@ -25,6 +25,10 @@ class LDAPTestSerializer(serializers.Serializer): class LDAPUserSerializer(serializers.Serializer): id = serializers.CharField() username = serializers.CharField() + name = serializers.CharField() email = serializers.CharField() existing = serializers.BooleanField(read_only=True) + +class PublicSettingSerializer(serializers.Serializer): + data = serializers.DictField(read_only=True) diff --git a/apps/settings/tasks/__init__.py b/apps/settings/tasks/__init__.py new file mode 100644 index 000000000..87bc6198f --- /dev/null +++ b/apps/settings/tasks/__init__.py @@ -0,0 +1,4 @@ +# coding: utf-8 +# + +from .ldap import * diff --git a/apps/settings/tasks/ldap.py b/apps/settings/tasks/ldap.py new file mode 100644 index 000000000..60058e03e --- /dev/null +++ b/apps/settings/tasks/ldap.py @@ -0,0 +1,17 @@ +# coding: utf-8 +# + +from celery import shared_task + +from common.utils import get_logger +from ..utils import LDAPSyncUtil + +__all__ = ['sync_ldap_user_task'] + + +logger = get_logger(__file__) + + +@shared_task +def sync_ldap_user_task(): + LDAPSyncUtil().perform_sync() diff --git a/apps/settings/templates/settings/_ldap_list_users_modal.html b/apps/settings/templates/settings/_ldap_list_users_modal.html index dd839eb5b..8589b0066 100644 --- a/apps/settings/templates/settings/_ldap_list_users_modal.html +++ b/apps/settings/templates/settings/_ldap_list_users_modal.html @@ -23,6 +23,7 @@
+ @@ -36,6 +37,9 @@
+
+
{% trans 'Loading' %}...
+
@@ -43,8 +47,11 @@ diff --git a/apps/settings/templates/settings/ldap_setting.html b/apps/settings/templates/settings/ldap_setting.html index 694fb66f9..5da66405d 100644 --- a/apps/settings/templates/settings/ldap_setting.html +++ b/apps/settings/templates/settings/ldap_setting.html @@ -63,9 +63,8 @@
-{# #} - +
@@ -109,33 +108,6 @@ $(document).ready(function () { error: error }); }) -.on("click","#btn_ldap_modal_confirm",function () { - var username_list = ldap_users_table.selected; - - if (username_list.length === 0){ - var msg = "{% trans 'User is not currently selected, please check the user you want to import'%}"; - toastr.error(msg); - return - } - - var the_url = "{% url "api-settings:ldap-user-sync" %}"; - - function error(message) { - toastr.error(message) - } - - function success(message) { - toastr.success(message.msg) - } - requestApi({ - url: the_url, - body: JSON.stringify({'username_list':username_list}), - method: "POST", - flash_message: false, - success: success, - error: error - }); - }) {% endblock %} diff --git a/apps/settings/urls/api_urls.py b/apps/settings/urls/api_urls.py index bc2e4731f..544de3941 100644 --- a/apps/settings/urls/api_urls.py +++ b/apps/settings/urls/api_urls.py @@ -10,9 +10,11 @@ urlpatterns = [ path('mail/testing/', api.MailTestingAPI.as_view(), name='mail-testing'), path('ldap/testing/', api.LDAPTestingAPI.as_view(), name='ldap-testing'), path('ldap/users/', api.LDAPUserListApi.as_view(), name='ldap-user-list'), - path('ldap/users/sync/', api.LDAPUserSyncAPI.as_view(), name='ldap-user-sync'), + path('ldap/users/import/', api.LDAPUserImportAPI.as_view(), name='ldap-user-import'), + path('ldap/cache/refresh/', api.LDAPCacheRefreshAPI.as_view(), name='ldap-cache-refresh'), path('terminal/replay-storage/create/', api.ReplayStorageCreateAPI.as_view(), name='replay-storage-create'), path('terminal/replay-storage/delete/', api.ReplayStorageDeleteAPI.as_view(), name='replay-storage-delete'), path('terminal/command-storage/create/', api.CommandStorageCreateAPI.as_view(), name='command-storage-create'), path('terminal/command-storage/delete/', api.CommandStorageDeleteAPI.as_view(), name='command-storage-delete'), + path('public/', api.PublicSettingApi.as_view(), name='public-setting'), ] diff --git a/apps/settings/utils.py b/apps/settings/utils.py deleted file mode 100644 index 9ecd5d286..000000000 --- a/apps/settings/utils.py +++ /dev/null @@ -1,219 +0,0 @@ -# -*- coding: utf-8 -*- -# - -from ldap3 import Server, Connection -from django.utils.translation import ugettext_lazy as _ - -from users.models import User -from users.utils import construct_user_email -from common.utils import get_logger -from common.const import LDAP_AD_ACCOUNT_DISABLE - -from .models import settings - - -logger = get_logger(__file__) - - -class LDAPOUGroupException(Exception): - pass - - -class LDAPUtil: - _conn = None - - SEARCH_FIELD_ALL = 'all' - SEARCH_FIELD_USERNAME = 'username' - - def __init__(self, use_settings_config=True, server_uri=None, bind_dn=None, - password=None, use_ssl=None, search_ougroup=None, - search_filter=None, attr_map=None, auth_ldap=None): - # config - self.paged_size = settings.AUTH_LDAP_SEARCH_PAGED_SIZE - - if use_settings_config: - self._load_config_from_settings() - else: - self.server_uri = server_uri - self.bind_dn = bind_dn - self.password = password - self.use_ssl = use_ssl - self.search_ougroup = search_ougroup - self.search_filter = search_filter - self.attr_map = attr_map - self.auth_ldap = auth_ldap - - def _load_config_from_settings(self): - self.server_uri = settings.AUTH_LDAP_SERVER_URI - self.bind_dn = settings.AUTH_LDAP_BIND_DN - self.password = settings.AUTH_LDAP_BIND_PASSWORD - self.use_ssl = settings.AUTH_LDAP_START_TLS - self.search_ougroup = settings.AUTH_LDAP_SEARCH_OU - self.search_filter = settings.AUTH_LDAP_SEARCH_FILTER - self.attr_map = settings.AUTH_LDAP_USER_ATTR_MAP - self.auth_ldap = settings.AUTH_LDAP - - @property - def connection(self): - if self._conn is None: - server = Server(self.server_uri, use_ssl=self.use_ssl) - conn = Connection(server, self.bind_dn, self.password) - conn.bind() - self._conn = conn - return self._conn - - @staticmethod - def get_user_by_username(username): - try: - user = User.objects.get(username=username) - except Exception as e: - return None - else: - return user - - def _ldap_entry_to_user_item(self, entry): - user_item = {} - for attr, mapping in self.attr_map.items(): - if not hasattr(entry, mapping): - continue - value = getattr(entry, mapping).value or '' - if mapping.lower() == 'useraccountcontrol' and attr == 'is_active'\ - and value: - value = int(value) & LDAP_AD_ACCOUNT_DISABLE \ - != LDAP_AD_ACCOUNT_DISABLE - user_item[attr] = value - return user_item - - def _search_user_items_ou(self, search_ou, extra_filter=None, cookie=None): - search_filter = self.search_filter % {"user": "*"} - if extra_filter: - search_filter = '(&{}{})'.format(search_filter, extra_filter) - - ok = self.connection.search( - search_ou, search_filter, - attributes=list(self.attr_map.values()), - paged_size=self.paged_size, paged_cookie=cookie - ) - if not ok: - error = _("Search no entry matched in ou {}".format(search_ou)) - raise LDAPOUGroupException(error) - - user_items = [] - for entry in self.connection.entries: - user_item = self._ldap_entry_to_user_item(entry) - user = self.get_user_by_username(user_item['username']) - user_item['existing'] = bool(user) - if user_item in user_items: - continue - user_items.append(user_item) - return user_items - - def _cookie(self): - if self.paged_size is None: - cookie = None - else: - cookie = self.connection.result['controls']['1.2.840.113556.1.4.319']['value']['cookie'] - return cookie - - def search_user_items(self, extra_filter=None): - user_items = [] - logger.info("Search user items") - - for search_ou in str(self.search_ougroup).split("|"): - logger.info("Search user search ou: {}".format(search_ou)) - _user_items = self._search_user_items_ou(search_ou, extra_filter=extra_filter) - user_items.extend(_user_items) - while self._cookie(): - logger.info("Page Search user search ou: {}".format(search_ou)) - _user_items = self._search_user_items_ou(search_ou, extra_filter, self._cookie()) - user_items.extend(_user_items) - logger.info("Search user items end") - return user_items - - def construct_extra_filter(self, field, q): - if not q: - return None - extra_filter = '' - if field == self.SEARCH_FIELD_ALL: - for attr in self.attr_map.values(): - extra_filter += '({}={})'.format(attr, q) - extra_filter = '(|{})'.format(extra_filter) - return extra_filter - - if field == self.SEARCH_FIELD_USERNAME and isinstance(q, list): - attr = self.attr_map.get('username') - for username in q: - extra_filter += '({}={})'.format(attr, username) - extra_filter = '(|{})'.format(extra_filter) - return extra_filter - - def search_filter_user_items(self, username_list): - extra_filter = self.construct_extra_filter( - self.SEARCH_FIELD_USERNAME, username_list - ) - user_items = self.search_user_items(extra_filter) - return user_items - - @staticmethod - def save_user(user, user_item): - for field, value in user_item.items(): - if not hasattr(user, field): - continue - if isinstance(getattr(user, field), bool): - if isinstance(value, str): - value = value.lower() - value = value in ['true', 1, True] - setattr(user, field, value) - user.save() - - def update_user(self, user_item): - user = self.get_user_by_username(user_item['username']) - if user.source != User.SOURCE_LDAP: - msg = _('The user source is not LDAP') - return False, msg - try: - self.save_user(user, user_item) - except Exception as e: - logger.error(e, exc_info=True) - return False, str(e) - else: - return True, None - - def create_user(self, user_item): - user = User(source=User.SOURCE_LDAP) - try: - self.save_user(user, user_item) - except Exception as e: - logger.error(e, exc_info=True) - return False, str(e) - else: - return True, None - - @staticmethod - def construct_user_email(user_item): - username = user_item['username'] - email = user_item.get('email', '') - email = construct_user_email(username, email) - return email - - def create_or_update_users(self, user_items): - succeed = failed = 0 - for user_item in user_items: - exist = user_item.pop('existing', False) - user_item['email'] = self.construct_user_email(user_item) - if not exist: - ok, error = self.create_user(user_item) - else: - ok, error = self.update_user(user_item) - if not ok: - logger.info("Failed User: {}".format(user_item)) - failed += 1 - else: - succeed += 1 - result = {'total': len(user_items), 'succeed': succeed, 'failed': failed} - return result - - def sync_users(self, username_list=None): - user_items = self.search_filter_user_items(username_list) - result = self.create_or_update_users(user_items) - return result diff --git a/apps/settings/utils/__init__.py b/apps/settings/utils/__init__.py new file mode 100644 index 000000000..87bc6198f --- /dev/null +++ b/apps/settings/utils/__init__.py @@ -0,0 +1,4 @@ +# coding: utf-8 +# + +from .ldap import * diff --git a/apps/settings/utils/ldap.py b/apps/settings/utils/ldap.py new file mode 100644 index 000000000..e5ad1c3e8 --- /dev/null +++ b/apps/settings/utils/ldap.py @@ -0,0 +1,338 @@ +# coding: utf-8 +# + +from ldap3 import Server, Connection +from django.conf import settings +from django.core.cache import cache +from django.utils.translation import ugettext_lazy as _ + +from common.const import LDAP_AD_ACCOUNT_DISABLE +from common.utils import timeit, get_logger +from users.utils import construct_user_email +from users.models import User + +logger = get_logger(__file__) + +__all__ = [ + 'LDAPConfig', 'LDAPServerUtil', 'LDAPCacheUtil', 'LDAPImportUtil', + 'LDAPSyncUtil', 'LDAP_USE_CACHE_FLAGS' +] + +LDAP_USE_CACHE_FLAGS = [1, '1', 'true', 'True', True] + + +class LDAPOUGroupException(Exception): + pass + + +class LDAPConfig(object): + + def __init__(self, config=None): + self.server_uri = None + self.bind_dn = None + self.password = None + self.use_ssl = None + self.search_ougroup = None + self.search_filter = None + self.attr_map = None + if isinstance(config, dict): + self.load_from_config(config) + else: + self.load_from_settings() + + def load_from_config(self, config): + self.server_uri = config.get('server_uri') + self.bind_dn = config.get('bind_dn') + self.password = config.get('password') + self.use_ssl = config.get('use_ssl') + self.search_ougroup = config.get('search_ougroup') + self.search_filter = config.get('search_filter') + self.attr_map = config.get('attr_map') + + def load_from_settings(self): + self.server_uri = settings.AUTH_LDAP_SERVER_URI + self.bind_dn = settings.AUTH_LDAP_BIND_DN + self.password = settings.AUTH_LDAP_BIND_PASSWORD + self.use_ssl = settings.AUTH_LDAP_START_TLS + self.search_ougroup = settings.AUTH_LDAP_SEARCH_OU + self.search_filter = settings.AUTH_LDAP_SEARCH_FILTER + self.attr_map = settings.AUTH_LDAP_USER_ATTR_MAP + + +class LDAPServerUtil(object): + + def __init__(self, config=None): + if isinstance(config, dict): + self.config = LDAPConfig(config=config) + elif isinstance(config, LDAPConfig): + self.config = config + else: + self.config = LDAPConfig() + self._conn = None + self._paged_size = self.get_paged_size() + self.search_users = None + self.search_value = None + + @property + def connection(self): + if self._conn: + return self._conn + server = Server(self.config.server_uri, use_ssl=self.config.use_ssl) + conn = Connection(server, self.config.bind_dn, self.config.password) + conn.bind() + self._conn = conn + return self._conn + + @staticmethod + def get_paged_size(): + paged_size = settings.AUTH_LDAP_SEARCH_PAGED_SIZE + if isinstance(paged_size, int): + return paged_size + return None + + def paged_cookie(self): + if self._paged_size is None: + return None + cookie = self.connection.result['controls']['1.2.840.113556.1.4.319']['value']['cookie'] + return cookie + + def get_search_filter_extra(self): + extra = '' + if self.search_users: + mapping_username = self.config.attr_map.get('username') + for user in self.search_users: + extra += '({}={})'.format(mapping_username, user) + return '(|{})'.format(extra) + if self.search_value: + for attr in self.config.attr_map.values(): + extra += '({}={})'.format(attr, self.search_value) + return '(|{})'.format(extra) + return extra + + def get_search_filter(self): + search_filter = self.config.search_filter % {'user': '*'} + search_filter_extra = self.get_search_filter_extra() + if search_filter_extra: + search_filter = '(&{}{})'.format(search_filter, search_filter_extra) + return search_filter + + def search_user_entries_ou(self, search_ou, paged_cookie=None): + search_filter = self.get_search_filter() + attributes = list(self.config.attr_map.values()) + ok = self.connection.search( + search_base=search_ou, search_filter=search_filter, + attributes=attributes, paged_size=self._paged_size, + paged_cookie=paged_cookie + ) + if not ok: + error = _("Search no entry matched in ou {}".format(search_ou)) + raise LDAPOUGroupException(error) + + @timeit + def search_user_entries(self): + logger.info("Search user entries") + user_entries = list() + search_ous = str(self.config.search_ougroup).split('|') + for search_ou in search_ous: + logger.info("Search user entries ou: {}".format(search_ou)) + self.search_user_entries_ou(search_ou) + user_entries.extend(self.connection.entries) + while self.paged_cookie(): + self.search_user_entries_ou(search_ou, self.paged_cookie()) + user_entries.extend(self.connection.entries) + return user_entries + + def user_entry_to_dict(self, entry): + user = {} + attr_map = self.config.attr_map.items() + for attr, mapping in attr_map: + if not hasattr(entry, mapping): + continue + value = getattr(entry, mapping).value or '' + if attr == 'is_active' and mapping.lower() == 'useraccountcontrol' \ + and value: + value = int(value) & LDAP_AD_ACCOUNT_DISABLE != LDAP_AD_ACCOUNT_DISABLE + user[attr] = value + return user + + @timeit + def user_entries_to_dict(self, user_entries): + users = [] + for user_entry in user_entries: + user = self.user_entry_to_dict(user_entry) + users.append(user) + return users + + @timeit + def search(self, search_users=None, search_value=None): + logger.info("Search ldap users") + self.search_users = search_users + self.search_value = search_value + user_entries = self.search_user_entries() + users = self.user_entries_to_dict(user_entries) + return users + + +class LDAPCacheUtil(object): + CACHE_KEY_USERS = 'CACHE_KEY_LDAP_USERS' + + def __init__(self): + self.search_users = None + self.search_value = None + + def set_users(self, users): + logger.info('Set ldap users to cache, count: {}'.format(len(users))) + cache.set(self.CACHE_KEY_USERS, users, None) + + def get_users(self): + users = cache.get(self.CACHE_KEY_USERS) + count = users if users is None else len(users) + logger.info('Get ldap users from cache, count: {}'.format(count)) + return users + + def delete_users(self): + logger.info('Delete ldap users from cache') + cache.delete(self.CACHE_KEY_USERS) + + def filter_users(self, users): + if users is None: + return users + if self.search_users: + filter_users = [ + user for user in users + if user['username'] in self.search_users + ] + elif self.search_value: + filter_users = [ + user for user in users + if self.search_value in ','.join(user.values()) + ] + else: + filter_users = users + return filter_users + + def search(self, search_users=None, search_value=None): + self.search_users = search_users + self.search_value = search_value + users = self.get_users() + users = self.filter_users(users) + return users + + +class LDAPSyncUtil(object): + CACHE_KEY_LDAP_USERS_SYNC_TASK_ERROR_MSG = 'CACHE_KEY_LDAP_USERS_SYNC_TASK_ERROR_MSG' + + CACHE_KEY_LDAP_USERS_SYNC_TASK_STATUS = 'CACHE_KEY_LDAP_USERS_SYNC_TASK_STATUS' + TASK_STATUS_IS_RUNNING = 'RUNNING' + TASK_STATUS_IS_OVER = 'OVER' + + def __init__(self): + self.server_util = LDAPServerUtil() + self.cache_util = LDAPCacheUtil() + self.task_error_msg = None + + def clear_cache(self): + logger.info('Clear ldap sync cache') + self.delete_task_status() + self.delete_task_error_msg() + self.cache_util.delete_users() + + @property + def task_no_start(self): + status = self.get_task_status() + return status is None + + @property + def task_is_running(self): + status = self.get_task_status() + return status == self.TASK_STATUS_IS_RUNNING + + @property + def task_is_over(self): + status = self.get_task_status() + return status == self.TASK_STATUS_IS_OVER + + def set_task_status(self, status): + logger.info('Set task status: {}'.format(status)) + cache.set(self.CACHE_KEY_LDAP_USERS_SYNC_TASK_STATUS, status, None) + + def get_task_status(self): + status = cache.get(self.CACHE_KEY_LDAP_USERS_SYNC_TASK_STATUS) + logger.info('Get task status: {}'.format(status)) + return status + + def delete_task_status(self): + logger.info('Delete task status') + cache.delete(self.CACHE_KEY_LDAP_USERS_SYNC_TASK_STATUS) + + def set_task_error_msg(self, error_msg): + logger.info('Set task error msg') + cache.set(self.CACHE_KEY_LDAP_USERS_SYNC_TASK_ERROR_MSG, error_msg, None) + + def get_task_error_msg(self): + logger.info('Get task error msg') + error_msg = cache.get(self.CACHE_KEY_LDAP_USERS_SYNC_TASK_ERROR_MSG) + return error_msg + + def delete_task_error_msg(self): + logger.info('Delete task error msg') + cache.delete(self.CACHE_KEY_LDAP_USERS_SYNC_TASK_ERROR_MSG) + + def pre_sync(self): + self.set_task_status(self.TASK_STATUS_IS_RUNNING) + + def sync(self): + users = self.server_util.search() + self.cache_util.set_users(users) + + def post_sync(self): + self.set_task_status(self.TASK_STATUS_IS_OVER) + + def perform_sync(self): + logger.info('Start perform sync ldap users from server to cache') + self.pre_sync() + try: + self.sync() + except Exception as e: + error_msg = str(e) + logger.error(error_msg) + self.set_task_error_msg(error_msg) + self.post_sync() + logger.info('End perform sync ldap users from server to cache') + + +class LDAPImportUtil(object): + + def __init__(self): + pass + + @staticmethod + def get_user_email(user): + username = user['username'] + email = user['email'] + email = construct_user_email(username, email) + return email + + def update_or_create(self, user): + user['email'] = self.get_user_email(user) + if user['username'] not in ['admin']: + user['source'] = User.SOURCE_LDAP + obj, created = User.objects.update_or_create( + username=user['username'], defaults=user + ) + return obj, created + + def perform_import(self, users): + logger.info('Start perform import ldap users, count: {}'.format(len(users))) + errors = [] + for user in users: + try: + self.update_or_create(user) + except Exception as e: + errors.append({user['username']: str(e)}) + logger.error(e) + logger.info('End perform import ldap users') + return errors + + + diff --git a/apps/settings/views.py b/apps/settings/views.py index a9df717d7..2442f074e 100644 --- a/apps/settings/views.py +++ b/apps/settings/views.py @@ -5,6 +5,7 @@ from django.utils.translation import ugettext as _ from common.permissions import PermissionsMixin, IsSuperUser from common import utils +from .utils import LDAPSyncUtil from .forms import EmailSettingForm, LDAPSettingForm, BasicSettingForm, \ TerminalSettingForm, SecuritySettingForm, EmailContentSettingForm @@ -83,6 +84,7 @@ class LDAPSettingView(PermissionsMixin, TemplateView): form.save() msg = _("Update setting successfully") messages.success(request, msg) + LDAPSyncUtil().clear_cache() return redirect('settings:ldap-setting') else: context = self.get_context_data() diff --git a/apps/users/forms.py b/apps/users/forms.py index 98d7c9e09..649f66ab9 100644 --- a/apps/users/forms.py +++ b/apps/users/forms.py @@ -2,6 +2,7 @@ from django import forms from django.utils.translation import gettext_lazy as _ +from django.conf import settings from common.utils import validate_ssh_public_key from orgs.mixins.forms import OrgModelForm @@ -21,6 +22,20 @@ class UserCheckOtpCodeForm(forms.Form): otp_code = forms.CharField(label=_('MFA code'), max_length=6) +def get_source_choices(): + choices_all = dict(User.SOURCE_CHOICES) + choices = [ + (User.SOURCE_LOCAL, choices_all[User.SOURCE_LOCAL]), + ] + if settings.AUTH_LDAP: + choices.append((User.SOURCE_LDAP, choices_all[User.SOURCE_LDAP])) + if settings.AUTH_OPENID: + choices.append((User.SOURCE_OPENID, choices_all[User.SOURCE_OPENID])) + if settings.AUTH_RADIUS: + choices.append((User.SOURCE_RADIUS, choices_all[User.SOURCE_RADIUS])) + return choices + + class UserCreateUpdateFormMixin(OrgModelForm): role_choices = ((i, n) for i, n in User.ROLE_CHOICES if i != User.ROLE_APP) password = forms.CharField( @@ -31,6 +46,10 @@ class UserCreateUpdateFormMixin(OrgModelForm): choices=role_choices, required=True, initial=User.ROLE_USER, label=_("Role") ) + source = forms.ChoiceField( + choices=get_source_choices, required=True, + initial=User.SOURCE_LOCAL, label=_("Source") + ) public_key = forms.CharField( label=_('ssh public key'), max_length=5000, required=False, widget=forms.Textarea(attrs={'placeholder': _('ssh-rsa AAAA...')}), @@ -41,7 +60,8 @@ class UserCreateUpdateFormMixin(OrgModelForm): model = User fields = [ 'username', 'name', 'email', 'groups', 'wechat', - 'phone', 'role', 'date_expired', 'comment', 'otp_level' + 'source', 'phone', 'role', 'date_expired', + 'comment', 'otp_level' ] widgets = { 'otp_level': forms.RadioSelect(), diff --git a/apps/users/serializers/__init__.py b/apps/users/serializers/__init__.py index 94ef71f28..78a695e51 100644 --- a/apps/users/serializers/__init__.py +++ b/apps/users/serializers/__init__.py @@ -1,3 +1,4 @@ # -*- coding: utf-8 -*- # -from .v1 import * \ No newline at end of file +from .user import * +from .group import * diff --git a/apps/users/serializers/group.py b/apps/users/serializers/group.py new file mode 100644 index 000000000..d27ddc19a --- /dev/null +++ b/apps/users/serializers/group.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +# +from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers + +from common.fields import StringManyToManyField +from common.serializers import AdaptedBulkListSerializer +from orgs.mixins.serializers import BulkOrgResourceModelSerializer +from ..models import User, UserGroup +from .. import utils + + +__all__ = [ + 'UserGroupSerializer', 'UserGroupListSerializer', + 'UserGroupUpdateMemberSerializer' +] + + +class UserGroupSerializer(BulkOrgResourceModelSerializer): + users = serializers.PrimaryKeyRelatedField( + required=False, many=True, queryset=User.objects, label=_('User') + ) + + class Meta: + model = UserGroup + list_serializer_class = AdaptedBulkListSerializer + fields = [ + 'id', 'name', 'users', 'comment', 'date_created', + 'created_by', + ] + extra_kwargs = { + 'created_by': {'label': _('Created by'), 'read_only': True} + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.set_fields_queryset() + + def set_fields_queryset(self): + users_field = self.fields['users'] + users_field.child_relation.queryset = utils.get_current_org_members() + + def validate_users(self, users): + for user in users: + if user.is_super_auditor: + msg = _('Auditors cannot be join in the user group') + raise serializers.ValidationError(msg) + return users + + +class UserGroupListSerializer(UserGroupSerializer): + users = StringManyToManyField(many=True, read_only=True) + + +class UserGroupUpdateMemberSerializer(serializers.ModelSerializer): + users = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects) + + class Meta: + model = UserGroup + fields = ['id', 'users'] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.set_fields_queryset() + + def set_fields_queryset(self): + users_field = self.fields['users'] + users_field.child_relation.queryset = utils.get_current_org_members() + diff --git a/apps/users/serializers/v1.py b/apps/users/serializers/user.py similarity index 69% rename from apps/users/serializers/v1.py rename to apps/users/serializers/user.py index 847afe885..57e2f43fa 100644 --- a/apps/users/serializers/v1.py +++ b/apps/users/serializers/user.py @@ -6,19 +6,14 @@ from rest_framework import serializers from common.utils import validate_ssh_public_key from common.mixins import BulkSerializerMixin -from common.fields import StringManyToManyField from common.serializers import AdaptedBulkListSerializer from common.permissions import CanUpdateDeleteUser -from orgs.mixins.serializers import BulkOrgResourceModelSerializer from ..models import User, UserGroup -from .. import utils __all__ = [ 'UserSerializer', 'UserPKUpdateSerializer', 'UserUpdateGroupSerializer', - 'UserGroupSerializer', 'UserGroupListSerializer', - 'UserGroupUpdateMemberSerializer', 'ChangeUserPasswordSerializer', - 'ResetOTPSerializer', + 'ChangeUserPasswordSerializer', 'ResetOTPSerializer', ] @@ -49,7 +44,6 @@ class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer): 'is_valid': {'label': _('Is valid')}, 'is_expired': {'label': _('Is expired')}, 'avatar_url': {'label': _('Avatar url')}, - 'source': {'read_only': True}, 'created_by': {'read_only': True, 'allow_blank': True}, 'can_update': {'read_only': True}, 'can_delete': {'read_only': True}, @@ -127,58 +121,6 @@ class UserUpdateGroupSerializer(serializers.ModelSerializer): fields = ['id', 'groups'] -class UserGroupSerializer(BulkOrgResourceModelSerializer): - users = serializers.PrimaryKeyRelatedField( - required=False, many=True, queryset=User.objects, label=_('User') - ) - - class Meta: - model = UserGroup - list_serializer_class = AdaptedBulkListSerializer - fields = [ - 'id', 'name', 'users', 'comment', 'date_created', - 'created_by', - ] - extra_kwargs = { - 'created_by': {'label': _('Created by'), 'read_only': True} - } - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.set_fields_queryset() - - def set_fields_queryset(self): - users_field = self.fields['users'] - users_field.child_relation.queryset = utils.get_current_org_members() - - def validate_users(self, users): - for user in users: - if user.is_super_auditor: - msg = _('Auditors cannot be join in the user group') - raise serializers.ValidationError(msg) - return users - - -class UserGroupListSerializer(UserGroupSerializer): - users = StringManyToManyField(many=True, read_only=True) - - -class UserGroupUpdateMemberSerializer(serializers.ModelSerializer): - users = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects) - - class Meta: - model = UserGroup - fields = ['id', 'users'] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.set_fields_queryset() - - def set_fields_queryset(self): - users_field = self.fields['users'] - users_field.child_relation.queryset = utils.get_current_org_members() - - class ChangeUserPasswordSerializer(serializers.ModelSerializer): class Meta: diff --git a/apps/users/signals_handler.py b/apps/users/signals_handler.py index 4c6afc663..a33d9eec9 100644 --- a/apps/users/signals_handler.py +++ b/apps/users/signals_handler.py @@ -2,11 +2,11 @@ # from django.dispatch import receiver -# from django.db.models.signals import post_save +from django.db.models.signals import post_save, m2m_changed from common.utils import get_logger from .signals import post_user_create -# from .models import User +from .models import User logger = get_logger(__file__) @@ -28,3 +28,14 @@ def on_user_create(sender, user=None, **kwargs): logger.info(" - Sending welcome mail ...".format(user.name)) if user.email: send_user_created_mail(user) + + +@receiver(m2m_changed, sender=User.groups.through) +def on_user_groups_change(sender, instance=None, action='', **kwargs): + """ + 资产节点发生变化时,刷新节点 + """ + if action.startswith('post'): + logger.debug("User group member change signal recv: {}".format(instance)) + from perms.utils import AssetPermissionUtilV2 + AssetPermissionUtilV2.expire_all_user_tree_cache() diff --git a/apps/users/tasks.py b/apps/users/tasks.py index e0051e939..45e1c40cd 100644 --- a/apps/users/tasks.py +++ b/apps/users/tasks.py @@ -1,17 +1,20 @@ # -*- coding: utf-8 -*- # +import sys from celery import shared_task from django.conf import settings -from ops.celery.utils import create_or_update_celery_periodic_tasks +from ops.celery.utils import ( + create_or_update_celery_periodic_tasks, disable_celery_periodic_task +) from ops.celery.decorator import after_app_ready_start from common.utils import get_logger from .models import User from .utils import ( send_password_expiration_reminder_mail, send_user_expiration_reminder_mail ) -from settings.utils import LDAPUtil +from settings.utils import LDAPServerUtil, LDAPImportUtil logger = get_logger(__file__) @@ -70,19 +73,26 @@ def check_user_expired_periodic(): @shared_task -def sync_ldap_user(): - logger.info("Start sync ldap user periodic task") - util = LDAPUtil() - result = util.sync_users() - logger.info("Result: {}".format(result)) +def import_ldap_user(): + logger.info("Start import ldap user task") + util_server = LDAPServerUtil() + util_import = LDAPImportUtil() + users = util_server.search() + errors = util_import.perform_import(users) + if errors: + logger.error("Imported LDAP users errors: {}".format(errors)) + else: + logger.info('Imported {} users successfully'.format(len(users))) @shared_task @after_app_ready_start -def sync_ldap_user_periodic(): +def import_ldap_user_periodic(): if not settings.AUTH_LDAP: return if not settings.AUTH_LDAP_SYNC_IS_PERIODIC: + task_name = sys._getframe().f_code.co_name + disable_celery_periodic_task(task_name) return interval = settings.AUTH_LDAP_SYNC_INTERVAL @@ -91,10 +101,9 @@ def sync_ldap_user_periodic(): else: interval = None crontab = settings.AUTH_LDAP_SYNC_CRONTAB - tasks = { - 'sync_ldap_user_periodic': { - 'task': sync_ldap_user.name, + 'import_ldap_user_periodic': { + 'task': import_ldap_user.name, 'interval': interval, 'crontab': crontab, 'enabled': True, diff --git a/apps/users/templates/users/_user.html b/apps/users/templates/users/_user.html index 192dbfb70..ea0f76854 100644 --- a/apps/users/templates/users/_user.html +++ b/apps/users/templates/users/_user.html @@ -21,6 +21,7 @@

{% trans 'Auth' %}

{% block password %}{% endblock %} {% bootstrap_field form.otp_level layout="horizontal" %} + {% bootstrap_field form.source layout="horizontal" %}

{% trans 'Security and Role' %}