From de3865fa1dbc4031bd1de56892ba9151120a9efa Mon Sep 17 00:00:00 2001 From: xinwen Date: Mon, 20 Jul 2020 10:42:22 +0800 Subject: [PATCH] =?UTF-8?q?refactor(orgs):=20=E9=87=8D=E6=9E=84=E7=BB=84?= =?UTF-8?q?=E7=BB=87=E8=A1=A8=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/audits/api.py | 4 +- apps/audits/filters.py | 2 +- apps/audits/models.py | 2 +- apps/common/const/choices.py | 8 + apps/common/db/aggregates.py | 7 +- apps/common/db/models.py | 48 ++++ apps/common/drf/fields.py | 43 +++ apps/jumpserver/api.py | 6 +- apps/locale/zh/LC_MESSAGES/django.mo | Bin 55392 -> 55579 bytes apps/locale/zh/LC_MESSAGES/django.po | 212 ++++++++------- apps/orgs/api.py | 24 +- .../migrations/0004_organizationmember.py | 33 +++ .../migrations/0005_auto_20200721_1937.py | 35 +++ .../migrations/0006_auto_20200721_1937.py | 32 +++ apps/orgs/models.py | 251 +++++++++++++----- apps/orgs/serializers.py | 42 ++- apps/orgs/signals_handler.py | 46 ++-- apps/orgs/tests.py | 17 +- apps/orgs/urls/api_urls.py | 6 - apps/users/api/mixins.py | 2 +- apps/users/api/user.py | 34 ++- apps/users/filters.py | 8 +- apps/users/forms/user.py | 4 +- apps/users/models/user.py | 101 ++++--- apps/users/serializers/user.py | 30 ++- apps/users/templates/users/user_detail.html | 2 +- apps/users/utils.py | 2 +- 27 files changed, 702 insertions(+), 299 deletions(-) create mode 100644 apps/common/const/choices.py create mode 100644 apps/common/db/models.py create mode 100644 apps/common/drf/fields.py create mode 100644 apps/orgs/migrations/0004_organizationmember.py create mode 100644 apps/orgs/migrations/0005_auto_20200721_1937.py create mode 100644 apps/orgs/migrations/0006_auto_20200721_1937.py diff --git a/apps/audits/api.py b/apps/audits/api.py index 7397b3596..0233bc1b2 100644 --- a/apps/audits/api.py +++ b/apps/audits/api.py @@ -43,7 +43,7 @@ class UserLoginLogViewSet(ListModelMixin, CommonGenericViewSet): @staticmethod def get_org_members(): - users = current_org.get_org_members().values_list('username', flat=True) + users = current_org.get_members().values_list('username', flat=True) return users def get_queryset(self): @@ -79,7 +79,7 @@ class PasswordChangeLogViewSet(ListModelMixin, CommonGenericViewSet): ordering = ['-datetime'] def get_queryset(self): - users = current_org.get_org_members() + users = current_org.get_members() queryset = super().get_queryset().filter( user__in=[user.__str__() for user in users] ) diff --git a/apps/audits/filters.py b/apps/audits/filters.py index 6db2d9b21..470c2c4b5 100644 --- a/apps/audits/filters.py +++ b/apps/audits/filters.py @@ -20,7 +20,7 @@ class CurrentOrgMembersFilter(filters.BaseFilterBackend): ] def _get_user_list(self): - users = current_org.get_org_members(exclude=('Auditor',)) + users = current_org.get_members(exclude=('Auditor',)) return users def filter_queryset(self, request, queryset, view): diff --git a/apps/audits/models.py b/apps/audits/models.py index 38a41554f..97aef40ce 100644 --- a/apps/audits/models.py +++ b/apps/audits/models.py @@ -124,7 +124,7 @@ class UserLoginLog(models.Model): Q(username__contains=keyword) ) if not current_org.is_root(): - username_list = current_org.get_org_members().values_list('username', flat=True) + username_list = current_org.get_members().values_list('username', flat=True) login_logs = login_logs.filter(username__in=username_list) return login_logs diff --git a/apps/common/const/choices.py b/apps/common/const/choices.py new file mode 100644 index 000000000..8de0c5fc8 --- /dev/null +++ b/apps/common/const/choices.py @@ -0,0 +1,8 @@ +from django.utils.translation import ugettext_lazy as _ + +from common.db.models import ChoiceSet + + +ADMIN = 'Admin' +USER = 'User' +AUDITOR = 'Auditor' diff --git a/apps/common/db/aggregates.py b/apps/common/db/aggregates.py index 081c1fea8..e04390299 100644 --- a/apps/common/db/aggregates.py +++ b/apps/common/db/aggregates.py @@ -3,10 +3,10 @@ from django.db.models import Aggregate class GroupConcat(Aggregate): function = 'GROUP_CONCAT' - template = '%(function)s(%(distinct)s %(expressions)s %(order_by)s %(separator))' + template = '%(function)s(%(expressions)s %(order_by)s %(separator)s)' allow_distinct = False - def __init__(self, expression, distinct=False, order_by=None, separator=',', **extra): + def __init__(self, expression, order_by=None, separator=',', **extra): order_by_clause = '' if order_by is not None: order = 'ASC' @@ -21,8 +21,7 @@ class GroupConcat(Aggregate): super().__init__( expression, - distinct='DISTINCT' if distinct else '', order_by=order_by_clause, - separator=f'SEPARATOR {separator}', + separator=f"SEPARATOR '{separator}'", **extra ) diff --git a/apps/common/db/models.py b/apps/common/db/models.py new file mode 100644 index 000000000..04d501b8f --- /dev/null +++ b/apps/common/db/models.py @@ -0,0 +1,48 @@ +from functools import partial + + +class Choice(str): + def __new__(cls, value, label): + self = super().__new__(cls, value) + self.label = label + return self + + +class ChoiceSetType(type): + def __new__(cls, name, bases, attrs): + _choices = [] + collected = set() + new_attrs = {} + for k, v in attrs.items(): + if isinstance(v, tuple): + v = Choice(*v) + assert v not in collected, 'Cannot be defined repeatedly' + _choices.append(v) + collected.add(v) + new_attrs[k] = v + for base in bases: + if hasattr(base, '_choices'): + for c in base._choices: + if c not in collected: + _choices.append(c) + collected.add(c) + new_attrs['_choices'] = _choices + new_attrs['_choices_dict'] = {c: c.label for c in _choices} + return type.__new__(cls, name, bases, new_attrs) + + def __contains__(self, item): + return self._choices_dict.__contains__(item) + + def __getitem__(self, item): + return self._choices_dict.__getitem__(item) + + def get(self, item, default=None): + return self._choices_dict.get(item, default=None) + + @property + def choices(self): + return [(c, c.label) for c in self._choices] + + +class ChoiceSet(metaclass=ChoiceSetType): + choices = None # 用于 Django Model 中的 choices 配置, 为了代码提示在此声明 diff --git a/apps/common/drf/fields.py b/apps/common/drf/fields.py new file mode 100644 index 000000000..e3b333d56 --- /dev/null +++ b/apps/common/drf/fields.py @@ -0,0 +1,43 @@ +from uuid import UUID + +from rest_framework.fields import get_attribute +from rest_framework.relations import ManyRelatedField, PrimaryKeyRelatedField, MANY_RELATION_KWARGS + + +class GroupConcatedManyRelatedField(ManyRelatedField): + def get_attribute(self, instance): + if hasattr(instance, 'pk') and instance.pk is None: + return [] + + attr = self.source_attrs[-1] + + # `gc` 是 `GroupConcat` 的缩写 + gc_attr = f'gc_{attr}' + if hasattr(instance, gc_attr): + gc_value = getattr(instance, gc_attr) + if isinstance(gc_value, str): + return [UUID(pk) for pk in set(gc_value.split(','))] + else: + return '' + + relationship = get_attribute(instance, self.source_attrs) + return relationship.all() if hasattr(relationship, 'all') else relationship + + +class GroupConcatedPrimaryKeyRelatedField(PrimaryKeyRelatedField): + @classmethod + def many_init(cls, *args, **kwargs): + list_kwargs = {'child_relation': cls(*args, **kwargs)} + for key in kwargs: + if key in MANY_RELATION_KWARGS: + list_kwargs[key] = kwargs[key] + return GroupConcatedManyRelatedField(**list_kwargs) + + def to_representation(self, value): + if self.pk_field is not None: + return self.pk_field.to_representation(value.pk) + + if hasattr(value, 'pk'): + return value.pk + else: + return value diff --git a/apps/jumpserver/api.py b/apps/jumpserver/api.py index 9558a92df..026a90b9a 100644 --- a/apps/jumpserver/api.py +++ b/apps/jumpserver/api.py @@ -128,7 +128,7 @@ class DatesLoginMetricMixin: @lazyproperty def dates_total_count_inactive_users(self): - total = current_org.get_org_members().count() + total = current_org.get_members().count() active = self.dates_total_count_active_users count = total - active if count < 0: @@ -137,7 +137,7 @@ class DatesLoginMetricMixin: @lazyproperty def dates_total_count_disabled_users(self): - return current_org.get_org_members().filter(is_active=False).count() + return current_org.get_members().filter(is_active=False).count() @lazyproperty def dates_total_count_active_assets(self): @@ -207,7 +207,7 @@ class DatesLoginMetricMixin: class TotalCountMixin: @staticmethod def get_total_count_users(): - return current_org.get_org_members().count() + return current_org.get_members().count() @staticmethod def get_total_count_assets(): diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 8fb1a75b49efdaf650b27b6d6e8ee016aab7d2dd..f72e19c129cdb9a828ece94b3631b84326a27fb3 100644 GIT binary patch delta 16951 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 delta 16766 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 PRXH-jC(h@1aLWG!Lh1O@ diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index b819ee494..dd64657a7 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: 2020-07-21 16:29+0800\n" +"POT-Creation-Date: 2020-07-27 19:01+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -25,10 +25,10 @@ msgstr "自定义" #: assets/models/asset.py:145 assets/models/base.py:232 #: assets/models/cluster.py:18 assets/models/cmd_filter.py:21 #: assets/models/domain.py:20 assets/models/group.py:20 -#: assets/models/label.py:18 ops/mixin.py:24 orgs/models.py:12 +#: assets/models/label.py:18 ops/mixin.py:24 orgs/models.py:22 #: perms/models/base.py:48 settings/models.py:27 terminal/models.py:26 #: terminal/models.py:342 terminal/models.py:374 terminal/models.py:411 -#: users/forms/profile.py:20 users/models/group.py:15 users/models/user.py:467 +#: users/forms/profile.py:20 users/models/group.py:15 users/models/user.py:473 #: users/templates/users/_select_user_modal.html:13 #: users/templates/users/user_asset_permission.html:37 #: users/templates/users/user_asset_permission.html:154 @@ -75,9 +75,9 @@ msgstr "数据库" #: assets/models/cmd_filter.py:23 assets/models/cmd_filter.py:57 #: assets/models/domain.py:21 assets/models/domain.py:54 #: assets/models/group.py:23 assets/models/label.py:23 ops/models/adhoc.py:37 -#: orgs/models.py:18 perms/models/base.py:56 settings/models.py:32 +#: orgs/models.py:25 perms/models/base.py:56 settings/models.py:32 #: terminal/models.py:36 terminal/models.py:381 terminal/models.py:418 -#: users/models/group.py:16 users/models/user.py:500 +#: users/models/group.py:16 users/models/user.py:506 #: users/templates/users/user_detail.html:115 #: users/templates/users/user_granted_database_app.html:38 #: users/templates/users/user_granted_remote_app.html:37 @@ -131,8 +131,8 @@ msgstr "参数" #: applications/models/remote_app.py:39 assets/models/asset.py:224 #: assets/models/base.py:240 assets/models/cluster.py:28 #: assets/models/cmd_filter.py:26 assets/models/cmd_filter.py:60 -#: assets/models/group.py:21 common/mixins/models.py:49 orgs/models.py:16 -#: perms/models/base.py:54 users/models/user.py:508 +#: assets/models/group.py:21 common/mixins/models.py:49 orgs/models.py:23 +#: orgs/models.py:316 perms/models/base.py:54 users/models/user.py:514 #: users/serializers/group.py:35 users/templates/users/user_detail.html:97 #: xpack/plugins/change_auth_plan/models.py:81 xpack/plugins/cloud/models.py:56 #: xpack/plugins/cloud/models.py:146 xpack/plugins/gathered_user/models.py:30 @@ -146,8 +146,8 @@ msgstr "创建者" #: assets/models/domain.py:23 assets/models/gathered_user.py:19 #: assets/models/group.py:22 assets/models/label.py:25 #: common/mixins/models.py:50 ops/models/adhoc.py:38 ops/models/command.py:27 -#: orgs/models.py:17 perms/models/base.py:55 users/models/group.py:18 -#: users/templates/users/user_group_detail.html:58 +#: orgs/models.py:24 orgs/models.py:314 perms/models/base.py:55 +#: users/models/group.py:18 users/templates/users/user_group_detail.html:58 #: xpack/plugins/cloud/models.py:59 xpack/plugins/cloud/models.py:149 msgid "Date created" msgstr "创建日期" @@ -336,10 +336,10 @@ msgid "AuthBook" msgstr "" #: assets/models/base.py:233 assets/models/gathered_user.py:15 -#: audits/models.py:99 authentication/forms.py:10 +#: audits/models.py:99 authentication/forms.py:11 #: authentication/templates/authentication/login.html:21 -#: authentication/templates/authentication/xpack_login.html:93 -#: ops/models/adhoc.py:148 users/forms/profile.py:19 users/models/user.py:465 +#: authentication/templates/authentication/xpack_login.html:101 +#: ops/models/adhoc.py:148 users/forms/profile.py:19 users/models/user.py:471 #: users/templates/users/_select_user_modal.html:14 #: users/templates/users/user_detail.html:53 #: users/templates/users/user_list.html:15 @@ -350,9 +350,9 @@ msgid "Username" msgstr "用户名" #: assets/models/base.py:234 assets/serializers/asset_user.py:71 -#: authentication/forms.py:12 +#: authentication/forms.py:13 #: authentication/templates/authentication/login.html:29 -#: authentication/templates/authentication/xpack_login.html:101 +#: authentication/templates/authentication/xpack_login.html:109 #: users/forms/user.py:22 users/forms/user.py:193 #: users/templates/users/user_otp_check_password.html:13 #: users/templates/users/user_password_update.html:43 @@ -379,7 +379,7 @@ msgid "SSH public key" msgstr "SSH公钥" #: assets/models/base.py:239 assets/models/gathered_user.py:20 -#: common/mixins/models.py:51 ops/models/adhoc.py:39 +#: common/mixins/models.py:51 ops/models/adhoc.py:39 orgs/models.py:315 msgid "Date updated" msgstr "更新日期" @@ -391,7 +391,7 @@ msgstr "带宽" msgid "Contact" msgstr "联系人" -#: assets/models/cluster.py:22 users/models/user.py:486 +#: assets/models/cluster.py:22 users/models/user.py:492 #: users/templates/users/user_detail.html:62 msgid "Phone" msgstr "手机" @@ -417,7 +417,7 @@ msgid "Default" msgstr "默认" #: assets/models/cluster.py:36 assets/models/label.py:14 -#: users/models/user.py:627 +#: users/models/user.py:635 msgid "System" msgstr "系统" @@ -535,14 +535,15 @@ msgstr "默认资产组" #: assets/models/label.py:15 audits/models.py:36 audits/models.py:56 #: audits/models.py:69 audits/serializers.py:77 authentication/models.py:43 -#: perms/forms/asset_permission.py:83 perms/forms/database_app_permission.py:38 +#: orgs/models.py:16 orgs/models.py:312 perms/forms/asset_permission.py:83 +#: perms/forms/database_app_permission.py:38 #: perms/forms/remote_app_permission.py:40 perms/models/base.py:49 #: templates/index.html:78 terminal/backends/command/models.py:18 #: terminal/backends/command/serializers.py:12 terminal/models.py:185 #: tickets/models/ticket.py:35 tickets/models/ticket.py:130 #: tickets/serializers/request_asset_perm.py:55 #: tickets/serializers/ticket.py:27 users/forms/group.py:15 -#: users/models/user.py:160 users/models/user.py:176 users/models/user.py:615 +#: users/models/user.py:157 users/models/user.py:623 #: users/serializers/group.py:20 #: users/templates/users/user_asset_permission.html:38 #: users/templates/users/user_asset_permission.html:64 @@ -707,14 +708,14 @@ msgid "Backend" msgstr "后端" #: assets/serializers/asset_user.py:75 users/forms/profile.py:148 -#: users/models/user.py:497 users/templates/users/user_password_update.html:48 +#: users/models/user.py:503 users/templates/users/user_password_update.html:48 #: users/templates/users/user_profile.html:69 #: users/templates/users/user_profile_update.html:46 #: users/templates/users/user_pubkey_update.html:46 msgid "Public key" msgstr "SSH公钥" -#: assets/serializers/asset_user.py:79 users/models/user.py:494 +#: assets/serializers/asset_user.py:79 users/models/user.py:500 msgid "Private key" msgstr "ssh私钥" @@ -999,8 +1000,8 @@ msgstr "Agent" #: audits/models.py:104 #: authentication/templates/authentication/_mfa_confirm_modal.html:14 #: authentication/templates/authentication/login_otp.html:6 -#: users/forms/profile.py:52 users/models/user.py:489 -#: users/serializers/user.py:220 users/templates/users/user_detail.html:77 +#: users/forms/profile.py:52 users/models/user.py:495 +#: users/serializers/user.py:224 users/templates/users/user_detail.html:77 #: users/templates/users/user_profile.html:87 msgid "MFA" msgstr "多因子认证" @@ -1169,7 +1170,10 @@ msgstr "等待登录复核处理" msgid "Login confirm ticket was {}" msgstr "登录复核 {}" -#: authentication/forms.py:29 users/forms/user.py:199 +#: authentication/forms.py:26 authentication/forms.py:34 +#: authentication/templates/authentication/login.html:38 +#: authentication/templates/authentication/xpack_login.html:118 +#: users/forms/user.py:199 msgid "MFA code" msgstr "多因子认证验证码" @@ -1224,7 +1228,7 @@ msgid "Show" msgstr "显示" #: authentication/templates/authentication/_access_key_modal.html:66 -#: users/models/user.py:387 users/serializers/user.py:217 +#: users/models/user.py:393 users/serializers/user.py:221 #: users/templates/users/user_profile.html:94 #: users/templates/users/user_profile.html:163 #: users/templates/users/user_profile.html:166 @@ -1233,7 +1237,7 @@ msgid "Disable" msgstr "禁用" #: authentication/templates/authentication/_access_key_modal.html:67 -#: users/models/user.py:388 users/serializers/user.py:218 +#: users/models/user.py:394 users/serializers/user.py:222 #: users/templates/users/user_profile.html:92 #: users/templates/users/user_profile.html:170 msgid "Enable" @@ -1274,29 +1278,29 @@ msgid "Code error" msgstr "代码错误" #: authentication/templates/authentication/login.html:6 -#: authentication/templates/authentication/login.html:39 -#: authentication/templates/authentication/xpack_login.html:112 +#: authentication/templates/authentication/login.html:49 +#: authentication/templates/authentication/xpack_login.html:130 #: templates/_base_only_msg_content.html:51 templates/_header_bar.html:83 msgid "Login" msgstr "登录" #: authentication/templates/authentication/login.html:17 -#: authentication/templates/authentication/xpack_login.html:87 +#: authentication/templates/authentication/xpack_login.html:95 msgid "Captcha invalid" msgstr "验证码错误" -#: authentication/templates/authentication/login.html:50 -#: authentication/templates/authentication/xpack_login.html:116 +#: authentication/templates/authentication/login.html:60 +#: authentication/templates/authentication/xpack_login.html:134 #: users/templates/users/forgot_password.html:7 #: users/templates/users/forgot_password.html:8 msgid "Forgot password" msgstr "忘记密码" -#: authentication/templates/authentication/login.html:57 +#: authentication/templates/authentication/login.html:67 msgid "More login options" msgstr "更多登录方式" -#: authentication/templates/authentication/login.html:61 +#: authentication/templates/authentication/login.html:71 msgid "OpenID" msgstr "OpenID" @@ -1337,11 +1341,11 @@ msgstr "返回" msgid "Copy success" msgstr "复制成功" -#: authentication/templates/authentication/xpack_login.html:74 +#: authentication/templates/authentication/xpack_login.html:78 msgid "Welcome back, please enter username and password to login" msgstr "欢迎回来,请输入用户名和密码登录" -#: authentication/views/login.py:83 +#: authentication/views/login.py:82 msgid "Please enable cookies and try again." msgstr "设置你的浏览器支持cookie" @@ -1602,11 +1606,11 @@ msgstr "命令 `{}` 不允许被执行 ......." msgid "Task end" msgstr "任务结束" -#: ops/tasks.py:68 +#: ops/tasks.py:71 msgid "Clean task history period" msgstr "定期清除任务历史" -#: ops/tasks.py:81 +#: ops/tasks.py:84 msgid "Clean celery log period" msgstr "定期清除Celery日志" @@ -1622,18 +1626,35 @@ msgstr "更新任务内容: {}" msgid "Disk used more than 80%: {} => {}" msgstr "磁盘使用率超过 80%: {} => {}" -#: orgs/api.py:57 +#: orgs/api.py:54 msgid "Organization contains undeleted resources" msgstr "组织内包含未删除的资源" -#: orgs/api.py:61 +#: orgs/api.py:58 msgid "The current organization cannot be deleted" msgstr "当能删除当前所在组织" -#: orgs/mixins/models.py:56 orgs/mixins/serializers.py:26 orgs/models.py:31 +#: orgs/mixins/models.py:56 orgs/mixins/serializers.py:26 orgs/models.py:40 +#: orgs/models.py:311 msgid "Organization" msgstr "组织" +#: orgs/models.py:15 +msgid "Organization administrator" +msgstr "组织管理员" + +#: orgs/models.py:17 +msgid "Organization auditor" +msgstr "组织审计员" + +#: orgs/models.py:313 users/forms/user.py:27 users/models/user.py:483 +#: users/templates/users/_select_user_modal.html:15 +#: users/templates/users/user_detail.html:73 +#: users/templates/users/user_list.html:16 +#: users/templates/users/user_profile.html:55 +msgid "Role" +msgstr "角色" + #: perms/const.py:7 msgid "Ungrouped" msgstr "未分组" @@ -1651,7 +1672,8 @@ msgstr "提示:RDP 协议不支持单独控制上传或下载文件" #: perms/forms/asset_permission.py:86 perms/forms/database_app_permission.py:41 #: perms/forms/remote_app_permission.py:43 perms/models/base.py:50 #: templates/_nav.html:21 users/forms/user.py:168 users/models/group.py:31 -#: users/models/user.py:473 users/templates/users/_select_user_modal.html:16 +#: users/models/user.py:479 users/serializers/user.py:43 +#: users/templates/users/_select_user_modal.html:16 #: users/templates/users/user_asset_permission.html:39 #: users/templates/users/user_asset_permission.html:67 #: users/templates/users/user_database_app_permission.html:38 @@ -1717,7 +1739,7 @@ msgid "Asset permission" msgstr "资产授权" #: perms/models/base.py:53 tickets/serializers/request_asset_perm.py:20 -#: users/models/user.py:505 users/templates/users/user_detail.html:93 +#: users/models/user.py:511 users/templates/users/user_detail.html:93 #: users/templates/users/user_profile.html:120 msgid "Date expired" msgstr "失效日期" @@ -2634,7 +2656,7 @@ msgstr "" " \n" " " -#: users/api/user.py:119 +#: users/api/user.py:126 msgid "Could not reset self otp, use profile reset instead" msgstr "不能在该页面重置多因子认证, 请去个人信息页面重置" @@ -2680,7 +2702,7 @@ msgstr "确认密码" msgid "Password does not match" msgstr "密码不一致" -#: users/forms/profile.py:89 users/models/user.py:469 +#: users/forms/profile.py:89 users/models/user.py:475 #: users/templates/users/user_detail.html:57 #: users/templates/users/user_profile.html:59 msgid "Email" @@ -2716,20 +2738,12 @@ msgid "Public key should not be the same as your old one." msgstr "不能和原来的密钥相同" #: users/forms/profile.py:137 users/forms/user.py:90 -#: users/serializers/user.py:181 users/serializers/user.py:262 -#: users/serializers/user.py:320 +#: users/serializers/user.py:185 users/serializers/user.py:266 +#: users/serializers/user.py:324 msgid "Not a valid ssh public key" msgstr "SSH密钥不合法" -#: users/forms/user.py:27 users/models/user.py:477 -#: users/templates/users/_select_user_modal.html:15 -#: users/templates/users/user_detail.html:73 -#: users/templates/users/user_list.html:16 -#: users/templates/users/user_profile.html:55 -msgid "Role" -msgstr "角色" - -#: users/forms/user.py:31 users/models/user.py:512 +#: users/forms/user.py:31 users/models/user.py:518 #: users/templates/users/user_detail.html:89 #: users/templates/users/user_list.html:18 #: users/templates/users/user_profile.html:102 @@ -2749,105 +2763,105 @@ msgstr "添加到用户组" msgid "* Your password does not meet the requirements" msgstr "* 您的密码不符合要求" -#: users/forms/user.py:124 users/serializers/user.py:30 +#: users/forms/user.py:124 users/serializers/user.py:31 msgid "Reset link will be generated and sent to the user" msgstr "生成重置密码链接,通过邮件发送给用户" -#: users/forms/user.py:125 users/serializers/user.py:31 +#: users/forms/user.py:125 users/serializers/user.py:32 msgid "Set password" msgstr "设置密码" -#: users/forms/user.py:132 users/serializers/user.py:38 +#: users/forms/user.py:132 users/serializers/user.py:39 #: xpack/plugins/change_auth_plan/models.py:61 #: xpack/plugins/change_auth_plan/serializers.py:30 msgid "Password strategy" msgstr "密码策略" -#: users/models/user.py:159 users/models/user.py:623 -msgid "Administrator" -msgstr "管理员" +#: users/models/user.py:156 +msgid "Super administrator" +msgstr "超级管理员" -#: users/models/user.py:161 +#: users/models/user.py:158 +msgid "Super auditor" +msgstr "超级审计员" + +#: users/models/user.py:159 msgid "Application" msgstr "应用程序" -#: users/models/user.py:162 -msgid "Auditor" -msgstr "审计员" - -#: users/models/user.py:172 -msgid "Org admin" -msgstr "组织管理员" - -#: users/models/user.py:174 -msgid "Org auditor" -msgstr "组织审计员" - -#: users/models/user.py:389 users/templates/users/user_profile.html:90 +#: users/models/user.py:395 users/templates/users/user_profile.html:90 msgid "Force enable" msgstr "强制启用" -#: users/models/user.py:456 +#: users/models/user.py:462 msgid "Local" msgstr "数据库" -#: users/models/user.py:480 +#: users/models/user.py:486 msgid "Avatar" msgstr "头像" -#: users/models/user.py:483 users/templates/users/user_detail.html:68 +#: users/models/user.py:489 users/templates/users/user_detail.html:68 msgid "Wechat" msgstr "微信" -#: users/models/user.py:516 +#: users/models/user.py:522 msgid "Date password last updated" msgstr "最后更新密码日期" -#: users/models/user.py:626 +#: users/models/user.py:631 +msgid "Administrator" +msgstr "管理员" + +#: users/models/user.py:634 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" -#: users/serializers/user.py:69 users/serializers/user.py:233 +#: users/serializers/user.py:72 users/serializers/user.py:237 msgid "Is first login" msgstr "首次登录" -#: users/serializers/user.py:70 +#: users/serializers/user.py:73 msgid "Is valid" msgstr "账户是否有效" -#: users/serializers/user.py:71 +#: users/serializers/user.py:74 msgid "Is expired" msgstr " 是否过期" -#: users/serializers/user.py:72 +#: users/serializers/user.py:75 msgid "Avatar url" msgstr "头像路径" -#: users/serializers/user.py:76 +#: users/serializers/user.py:79 msgid "Groups name" msgstr "用户组名" -#: users/serializers/user.py:77 +#: users/serializers/user.py:80 msgid "Source name" msgstr "用户来源名" -#: users/serializers/user.py:78 -msgid "Role name" -msgstr "角色名" +#: users/serializers/user.py:81 +msgid "Organization role name" +msgstr "组织角色名称" -#: users/serializers/user.py:101 +#: users/serializers/user.py:82 +msgid "Super role name" +msgstr "超级角色名称" + +#: users/serializers/user.py:105 msgid "Role limit to {}" msgstr "角色只能为 {}" -#: users/serializers/user.py:113 users/serializers/user.py:286 +#: users/serializers/user.py:117 users/serializers/user.py:290 msgid "Password does not match security rules" msgstr "密码不满足安全规则" -#: users/serializers/user.py:278 +#: users/serializers/user.py:282 msgid "The old password is incorrect" msgstr "旧密码错误" -#: users/serializers/user.py:292 +#: users/serializers/user.py:296 msgid "The newly set password is inconsistent" msgstr "两次密码不一致" @@ -3969,6 +3983,15 @@ msgstr "企业版" msgid "Ultimate edition" msgstr "旗舰版" +#~ msgid "Auditor" +#~ msgstr "审计员" + +#~ msgid "Org admin" +#~ msgstr "组织管理员" + +#~ msgid "Role name" +#~ msgstr "角色名" + #~ msgid "GUI copy" #~ msgstr "GUI 复制" @@ -5508,9 +5531,6 @@ msgstr "旗舰版" #~ msgid "Admin" #~ msgstr "管理员" -#~ msgid "Organizations" -#~ msgstr "组织管理" - #~ msgid "Org detail" #~ msgstr "组织详情" diff --git a/apps/orgs/api.py b/apps/orgs/api.py index d8ba32635..e29a14e22 100644 --- a/apps/orgs/api.py +++ b/apps/orgs/api.py @@ -1,23 +1,20 @@ # -*- coding: utf-8 -*- # -from django.shortcuts import get_object_or_404 from django.utils.translation import ugettext as _ from rest_framework import status, generics from rest_framework.views import Response from rest_framework_bulk import BulkModelViewSet from common.permissions import IsSuperUserOrAppUser -from .models import Organization +from .models import Organization, ROLE from .serializers import OrgSerializer, OrgReadSerializer, \ - OrgMembershipUserSerializer, OrgMembershipAdminSerializer, \ OrgAllUserSerializer, OrgRetrieveSerializer from users.models import User, UserGroup from assets.models import Asset, Domain, AdminUser, SystemUser, Label from perms.models import AssetPermission from orgs.utils import current_org from common.utils import get_logger -from .mixins.api import OrgMembershipModelViewSetMixin logger = get_logger(__file__) @@ -39,7 +36,7 @@ class OrgViewSet(BulkModelViewSet): def get_data_from_model(self, model): if model == User: - data = model.objects.filter(related_user_orgs__id=self.org.id) + data = model.objects.filter(orgs__id=self.org.id, m2m_org_members__role=ROLE.USER) else: data = model.objects.filter(org_id=self.org.id) return data @@ -64,18 +61,6 @@ class OrgViewSet(BulkModelViewSet): return Response({'msg': True}, status=status.HTTP_200_OK) -class OrgMembershipAdminsViewSet(OrgMembershipModelViewSetMixin, BulkModelViewSet): - serializer_class = OrgMembershipAdminSerializer - membership_class = Organization.admins.through - permission_classes = (IsSuperUserOrAppUser, ) - - -class OrgMembershipUsersViewSet(OrgMembershipModelViewSetMixin, BulkModelViewSet): - serializer_class = OrgMembershipUserSerializer - membership_class = Organization.users.through - permission_classes = (IsSuperUserOrAppUser, ) - - class OrgAllUserListApi(generics.ListAPIView): permission_classes = (IsSuperUserOrAppUser,) serializer_class = OrgAllUserSerializer @@ -84,6 +69,7 @@ class OrgAllUserListApi(generics.ListAPIView): def get_queryset(self): pk = self.kwargs.get("pk") - org = get_object_or_404(Organization, pk=pk) - users = org.get_org_users().only(*self.serializer_class.Meta.only_fields) + users = User.objects.filter( + orgs=pk, m2m_org_members__role=ROLE.USER + ).only(*self.serializer_class.Meta.only_fields) return users diff --git a/apps/orgs/migrations/0004_organizationmember.py b/apps/orgs/migrations/0004_organizationmember.py new file mode 100644 index 000000000..6b43a5850 --- /dev/null +++ b/apps/orgs/migrations/0004_organizationmember.py @@ -0,0 +1,33 @@ +# Generated by Django 2.2.10 on 2020-07-21 11:27 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('orgs', '0003_auto_20190916_1057'), + ] + + operations = [ + migrations.CreateModel( + name='OrganizationMember', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('role', models.CharField(choices=[('Admin', 'Administrator'), ('User', 'User'), ('Auditor', 'Auditor')], default='User', max_length=16, verbose_name='Role')), + ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')), + ('org', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='m2m_org_members', to='orgs.Organization', verbose_name='Organization')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='m2m_org_members', to=settings.AUTH_USER_MODEL, verbose_name='User')), + ], + options={ + 'db_table': 'orgs_organization_members', + 'unique_together': {('org', 'user', 'role')}, + }, + ), + ] diff --git a/apps/orgs/migrations/0005_auto_20200721_1937.py b/apps/orgs/migrations/0005_auto_20200721_1937.py new file mode 100644 index 000000000..ac0ec8a08 --- /dev/null +++ b/apps/orgs/migrations/0005_auto_20200721_1937.py @@ -0,0 +1,35 @@ +# Generated by Django 2.2.10 on 2020-07-21 11:37 + +from django.db import migrations + + +def migrate_old_organization_members(apps, schema_editor): + org_model = apps.get_model("orgs", "Organization") + org_member_model = apps.get_model('orgs', 'OrganizationMember') + orgs = org_model.objects.all() + + roles = ['User', 'Auditor', 'Admin'] + + for org in orgs: + users = org.users.all().only('id') + auditors = org.auditors.all().only('id') + admins = org.admins.all().only('id') + total_members = zip([users, auditors, admins], roles) + + org_members = [] + for members, role in total_members: + for user in members: + org_user = org_member_model(user=user, org=org, role=role) + org_members.append(org_user) + org_member_model.objects.bulk_create(org_members) + + +class Migration(migrations.Migration): + + dependencies = [ + ('orgs', '0004_organizationmember'), + ] + + operations = [ + migrations.RunPython(migrate_old_organization_members) + ] diff --git a/apps/orgs/migrations/0006_auto_20200721_1937.py b/apps/orgs/migrations/0006_auto_20200721_1937.py new file mode 100644 index 000000000..fe0b1f477 --- /dev/null +++ b/apps/orgs/migrations/0006_auto_20200721_1937.py @@ -0,0 +1,32 @@ +# Generated by Django 2.2.10 on 2020-07-21 11:37 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('orgs', '0005_auto_20200721_1937'), + ] + + operations = [ + migrations.RemoveField( + model_name='organization', + name='admins', + ), + migrations.RemoveField( + model_name='organization', + name='auditors', + ), + migrations.RemoveField( + model_name='organization', + name='users', + ), + migrations.AddField( + model_name='organization', + name='members', + field=models.ManyToManyField(related_name='orgs', through='orgs.OrganizationMember', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/apps/orgs/models.py b/apps/orgs/models.py index 6e6e6dfd6..effabf50f 100644 --- a/apps/orgs/models.py +++ b/apps/orgs/models.py @@ -1,21 +1,30 @@ import uuid -from django.conf import settings +from functools import partial from django.db import models +from django.db.models import signals +from django.db.models import Q from django.utils.translation import ugettext_lazy as _ -from common.utils import is_uuid, lazyproperty +from common.utils import is_uuid +from common.const import choices +from common.db.models import ChoiceSet + + +class ROLE(ChoiceSet): + ADMIN = choices.ADMIN, _('Organization administrator') + USER = choices.USER, _('User') + AUDITOR = choices.AUDITOR, _("Organization auditor") class Organization(models.Model): id = models.UUIDField(default=uuid.uuid4, primary_key=True) name = models.CharField(max_length=128, unique=True, verbose_name=_("Name")) - users = models.ManyToManyField('users.User', related_name='related_user_orgs', blank=True) - admins = models.ManyToManyField('users.User', related_name='related_admin_orgs', blank=True) - auditors = models.ManyToManyField('users.User', related_name='related_audit_orgs', blank=True) created_by = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by')) date_created = models.DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created')) comment = models.TextField(max_length=128, default='', blank=True, verbose_name=_('Comment')) + members = models.ManyToManyField('users.User', related_name='orgs', through='orgs.OrganizationMember', + through_fields=('org', 'user')) orgs = None CACHE_PREFIX = 'JMS_ORG_{}' @@ -72,29 +81,24 @@ class Organization(models.Model): org = cls.default() if default else None return org - # @lazyproperty - # lazyproperty 导致用户列表中角色显示出现不稳定的情况, 如果不加会导致数据库操作次数太多 - def org_users(self): + def get_org_members_by_role(self, role): from users.models import User if self.is_real(): - return self.users.all() - users = User.objects.filter(role=User.ROLE_USER) - if self.is_default() and not settings.DEFAULT_ORG_SHOW_ALL_USERS: - users = users.filter(related_user_orgs__isnull=True) + return self.members.filter(m2m_org_members__role=role) + users = User.objects.filter(role=role) return users - def get_org_users(self): - return self.org_users() + @property + def users(self): + return self.get_org_members_by_role(ROLE.USER) - # @lazyproperty - def org_admins(self): - from users.models import User - if self.is_real(): - return self.admins.all() - return User.objects.filter(role=User.ROLE_ADMIN) + @property + def admins(self): + return self.get_org_members_by_role(ROLE.ADMIN) - def get_org_admins(self): - return self.org_admins() + @property + def auditors(self): + return self.get_org_members_by_role(ROLE.AUDITOR) def org_id(self): if self.is_real(): @@ -104,87 +108,76 @@ class Organization(models.Model): else: return '' - # @lazyproperty - def org_auditors(self): + def get_members(self, exclude=()): from users.models import User if self.is_real(): - return self.auditors.all() - return User.objects.filter(role=User.ROLE_AUDITOR) + members = self.members.exclude(m2m_org_members__role__in=exclude) + else: + members = User.objects.exclude(role__in=exclude) - def get_org_auditors(self): - return self.org_auditors() - - def get_org_members(self, exclude=()): - from users.models import User - members = User.objects.none() - if 'Admin' not in exclude: - members |= self.get_org_admins() - if 'User' not in exclude: - members |= self.get_org_users() - if 'Auditor' not in exclude: - members |= self.get_org_auditors() - return members.exclude(role=User.ROLE_APP).distinct() + return members.exclude(role=User.ROLE.APP).distinct() def can_admin_by(self, user): if user.is_superuser: return True - if self.get_org_admins().filter(id=user.id): + if self.admins.filter(id=user.id).exists(): return True return False def can_audit_by(self, user): if user.is_super_auditor: return True - if self.get_org_auditors().filter(id=user.id): + if self.auditors.filter(id=user.id).exists(): return True return False def can_user_by(self, user): - if self.get_org_users().filter(id=user.id): + if self.users.filter(id=user.id).exists(): return True return False def is_real(self): return self.id not in (self.DEFAULT_NAME, self.ROOT_ID, self.SYSTEM_ID) + @classmethod + def get_user_orgs_by_role(cls, user, role): + if not isinstance(role, (tuple, list)): + role = (role, ) + + return cls.objects.filter( + m2m_org_members__role__in=role, + m2m_org_members__user_id=user.id + ).distinct() + @classmethod def get_user_admin_orgs(cls, user): - admin_orgs = [] if user.is_anonymous: - return admin_orgs - elif user.is_superuser: - admin_orgs = list(cls.objects.all()) - admin_orgs.append(cls.default()) - elif user.is_org_admin: - admin_orgs = user.related_admin_orgs.all() - return admin_orgs + return cls.objects.none() + if user.is_superuser: + return [*cls.objects.all(), cls.default()] + return cls.get_user_orgs_by_role(user, ROLE.ADMIN) @classmethod def get_user_user_orgs(cls, user): - user_orgs = [] if user.is_anonymous: - return user_orgs - user_orgs = user.related_user_orgs.all() - return user_orgs + return cls.objects.none() + return cls.get_user_orgs_by_role(user, ROLE.USER) @classmethod def get_user_audit_orgs(cls, user): - audit_orgs = [] if user.is_anonymous: - return audit_orgs - elif user.is_super_auditor: - audit_orgs = list(cls.objects.all()) - audit_orgs.append(cls.default()) - elif user.is_org_auditor: - audit_orgs = user.related_audit_orgs.all() - return audit_orgs + return cls.objects.none() + if user.is_super_auditor: + return [*cls.objects.all(), cls.default()] + return cls.get_user_orgs_by_role(user, ROLE.AUDITOR) @classmethod - def get_user_admin_or_audit_orgs(self, user): - admin_orgs = self.get_user_admin_orgs(user) - audit_orgs = self.get_user_audit_orgs(user) - orgs = set(admin_orgs) | set(audit_orgs) - return orgs + def get_user_admin_or_audit_orgs(cls, user): + if user.is_anonymous: + return cls.objects.none() + if user.is_superuser or user.is_super_auditor: + return [*cls.objects.all(), cls.default()] + return cls.get_user_orgs_by_role(user, (ROLE.AUDITOR, ROLE.ADMIN)) @classmethod def default(cls): @@ -211,8 +204,122 @@ class Organization(models.Model): from .utils import set_current_org set_current_org(self) - @classmethod - def all_orgs(cls): - orgs = list(cls.objects.all()) - orgs.append(cls.default()) - return orgs + +def _convert_to_uuid_set(users): + rst = set() + for user in users: + if isinstance(user, models.Model): + rst.add(user.id) + elif not isinstance(user, uuid.UUID): + rst.add(uuid.UUID(user)) + return rst + + +def _none2list(*args): + return ([] if v is None else v for v in args) + + +class OrgMemeberManager(models.Manager): + + def remove_users_by_role(self, org, users=None, admins=None, auditors=None): + if not any((users, admins, auditors)): + return + users, admins, auditors = _none2list(users, admins, auditors) + + send = partial(signals.m2m_changed.send, sender=self.model, instance=org, reverse=False, + model=Organization, pk_set=[*users, *admins, *auditors], using=self.db) + + send(action="pre_remove") + self.filter(org_id=org.id).filter( + Q(user__in=users, role=ROLE.USER) | + Q(user__in=admins, role=ROLE.ADMIN) | + Q(user__in=auditors, role=ROLE.AUDITOR) + ).delete() + send(action="post_remove") + + def add_users_by_role(self, org, users=None, admins=None, auditors=None): + if not any((users, admins, auditors)): + return + users, admins, auditors = _none2list(users, admins, auditors) + + add_mapper = ( + (users, ROLE.USER), + (admins, ROLE.ADMIN), + (auditors, ROLE.AUDITOR) + ) + + oms_add = [] + for users, role in add_mapper: + for user in users: + if isinstance(user, models.Model): + user = user.id + oms_add.append(self.model(org_id=org.id, user_id=user, role=role)) + + send = partial(signals.m2m_changed.send, sender=self.model, instance=org, reverse=False, + model=Organization, pk_set=[*users, *admins, *auditors], using=self.db) + + send(action='pre_add') + self.bulk_create(oms_add) + send(action='post_add') + + def _get_remove_add_set(self, new_users, old_users): + if new_users is None: + return None, None + new_users = _convert_to_uuid_set(new_users) + return (old_users - new_users), (new_users - old_users) + + def set_users_by_role(self, org, users=None, admins=None, auditors=None): + oms = self.filter(org_id=org.id).values_list('role', 'user_id') + + old_users, old_admins, old_auditors = set(), set(), set() + + mapper = { + ROLE.USER: old_users, + ROLE.ADMIN: old_admins, + ROLE.AUDITOR: old_auditors + } + + for role, user_id in oms: + if role in mapper: + mapper[role].add(user_id) + + users_remove, users_add = self._get_remove_add_set(users, old_users) + admins_remove, admins_add = self._get_remove_add_set(admins, old_admins) + auditors_remove, auditors_add = self._get_remove_add_set(auditors, old_auditors) + + self.remove_users_by_role( + org, + users_remove, + admins_remove, + auditors_remove + ) + + self.add_users_by_role( + org, + users_add, + admins_add, + auditors_add + ) + + +class OrganizationMember(models.Model): + """ + 注意:直接调用该 `Model.delete` `Model.objects.delete` 不会触发清理该用户的信号 + """ + + id = models.UUIDField(default=uuid.uuid4, primary_key=True) + org = models.ForeignKey(Organization, related_name='m2m_org_members', on_delete=models.CASCADE, verbose_name=_('Organization')) + user = models.ForeignKey('users.User', related_name='m2m_org_members', on_delete=models.CASCADE, verbose_name=_('User')) + role = models.CharField(max_length=16, choices=ROLE.choices, default=ROLE.USER, verbose_name=_("Role")) + date_created = models.DateTimeField(auto_now_add=True, verbose_name=_("Date created")) + date_updated = models.DateTimeField(auto_now=True, verbose_name=_("Date updated")) + created_by = models.CharField(max_length=128, null=True, verbose_name=_('Created by')) + + objects = OrgMemeberManager() + + class Meta: + unique_together = [('org', 'user', 'role')] + db_table = 'orgs_organization_members' + + def __str__(self): + return '{} is {}: {}'.format(self.user.name, self.org.name, self.role) diff --git a/apps/orgs/serializers.py b/apps/orgs/serializers.py index ae98420e0..4cf54f92a 100644 --- a/apps/orgs/serializers.py +++ b/apps/orgs/serializers.py @@ -1,16 +1,18 @@ from rest_framework.serializers import ModelSerializer from rest_framework import serializers -from users.models import UserGroup -from assets.models import Asset, Domain, AdminUser, SystemUser, Label -from perms.models import AssetPermission + +from users.models.user import User from common.serializers import AdaptedBulkListSerializer -from .utils import set_current_org, get_current_org -from .models import Organization +from .models import Organization, OrganizationMember from .mixins.serializers import OrgMembershipSerializerMixin class OrgSerializer(ModelSerializer): + users = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects.all(), write_only=True) + admins = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects.all(), write_only=True) + auditors = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects.all(), write_only=True) + class Meta: model = Organization list_serializer_class = AdaptedBulkListSerializer @@ -21,11 +23,27 @@ class OrgSerializer(ModelSerializer): fields_m2m = ['users', 'admins', 'auditors'] fields = fields_small + fields_m2m read_only_fields = ['created_by', 'date_created'] - extra_kwargs = { - 'admins': {'write_only': True}, - 'users': {'write_only': True}, - 'auditors': {'write_only': True}, - } + + def create(self, validated_data): + members = self._pop_memebers(validated_data) + instance = Organization.objects.create(**validated_data) + OrganizationMember.objects.add_users_by_role(instance, *members) + return instance + + def _pop_memebers(self, validated_data): + return ( + validated_data.pop('users', None), + validated_data.pop('admins', None), + validated_data.pop('auditors', None) + ) + + def update(self, instance, validated_data): + members = self._pop_memebers(validated_data) + for attr, value in validated_data.items(): + setattr(instance, attr, value) + instance.save() + OrganizationMember.objects.set_users_by_role(instance, *members) + return instance class OrgReadSerializer(OrgSerializer): @@ -34,14 +52,14 @@ class OrgReadSerializer(OrgSerializer): class OrgMembershipAdminSerializer(OrgMembershipSerializerMixin, ModelSerializer): class Meta: - model = Organization.admins.through + model = Organization.members.through list_serializer_class = AdaptedBulkListSerializer fields = '__all__' class OrgMembershipUserSerializer(OrgMembershipSerializerMixin, ModelSerializer): class Meta: - model = Organization.users.through + model = Organization.members.through list_serializer_class = AdaptedBulkListSerializer fields = '__all__' diff --git a/apps/orgs/signals_handler.py b/apps/orgs/signals_handler.py index 17e3d525e..eb7df9741 100644 --- a/apps/orgs/signals_handler.py +++ b/apps/orgs/signals_handler.py @@ -5,7 +5,7 @@ from django.db.models.signals import m2m_changed from django.db.models.signals import post_save from django.dispatch import receiver -from .models import Organization +from .models import Organization, OrganizationMember from .hands import set_current_org, current_org, Node, get_current_org from perms.models import AssetPermission from users.models import UserGroup @@ -26,23 +26,31 @@ def on_org_create_or_update(sender, instance=None, created=False, **kwargs): instance.expire_cache() -@receiver(m2m_changed, sender=Organization.users.through) -def on_org_user_changed(sender, instance=None, **kwargs): - if isinstance(instance, Organization): - old_org = current_org - set_current_org(instance) - if kwargs['action'] == 'pre_remove': - users = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) - for user in users: - perms = AssetPermission.objects.filter(users=user) - user_groups = UserGroup.objects.filter(users=user) - for perm in perms: - perm.users.remove(user) - for user_group in user_groups: - user_group.users.remove(user) - set_current_org(old_org) +def _remove_users(model, users, org, reverse=False): + if not isinstance(users, (tuple, list, set)): + users = (users, ) + + m2m_model = model.users.through + if reverse: + m2m_field_name = model.users.field.m2m_reverse_field_name() + else: + m2m_field_name = model.users.field.m2m_field_name() + m2m_model.objects.filter(**{'user__in': users, f'{m2m_field_name}__org_id': org.id}).delete() -@receiver(m2m_changed, sender=Organization.admins.through) -def on_org_admin_change(sender, **kwargs): - Organization._user_admin_orgs = None +def _clear_users_from_org(org, users): + if not users: + return + + old_org = current_org + set_current_org(org) + _remove_users(AssetPermission, users, org) + _remove_users(UserGroup, users, org, reverse=True) + set_current_org(old_org) + + +@receiver(m2m_changed, sender=OrganizationMember) +def on_org_user_changed(sender, instance=None, action=None, pk_set=None, **kwargs): + if action == 'post_remove': + leaved_users = set(pk_set) - set(instance.members.values_list('id', flat=True)) + _clear_users_from_org(instance, leaved_users) diff --git a/apps/orgs/tests.py b/apps/orgs/tests.py index 7ce503c2d..1b007fe3d 100644 --- a/apps/orgs/tests.py +++ b/apps/orgs/tests.py @@ -1,3 +1,16 @@ -from django.test import TestCase +from django.urls import reverse +from rest_framework.test import APITestCase +from rest_framework import status + +from users.models.user import User + + +class OrgTests(APITestCase): + def test_create(self): + print(User.objects.all()) + reverse('api-orgs:org-list') + + + +{"name":"a-07","admins":["138167d2-6843-4e25-b838-59657157c6c6"],"auditors":["8d4b3ec4-8339-4a2c-b33c-c2633da62c84"],"users":["ea60e8ce-876d-493b-a641-ff836258629c"]} -# Create your tests here. diff --git a/apps/orgs/urls/api_urls.py b/apps/orgs/urls/api_urls.py index 17f8c3c8a..e14435868 100644 --- a/apps/orgs/urls/api_urls.py +++ b/apps/orgs/urls/api_urls.py @@ -11,12 +11,6 @@ from .. import api app_name = 'orgs' router = DefaultRouter() -# 将会删除 -router.register(r'orgs/(?P[0-9a-zA-Z\-]{36})/membership/admins', - api.OrgMembershipAdminsViewSet, 'membership-admins') -router.register(r'orgs/(?P[0-9a-zA-Z\-]{36})/membership/users', - api.OrgMembershipUsersViewSet, 'membership-users'), - router.register(r'orgs', api.OrgViewSet, 'org') old_version_urlpatterns = [ diff --git a/apps/users/api/mixins.py b/apps/users/api/mixins.py index 117c2c28a..0e81bd8d2 100644 --- a/apps/users/api/mixins.py +++ b/apps/users/api/mixins.py @@ -9,7 +9,7 @@ from orgs.utils import current_org class UserQuerysetMixin: def get_queryset(self): if self.request.query_params.get('all') or not current_org.is_real(): - queryset = User.objects.exclude(role=User.ROLE_APP) + queryset = User.objects.exclude(role=User.ROLE.APP) else: queryset = utils.get_current_org_members() return queryset diff --git a/apps/users/api/user.py b/apps/users/api/user.py index 3ad748316..b1c039318 100644 --- a/apps/users/api/user.py +++ b/apps/users/api/user.py @@ -1,11 +1,13 @@ # ~*~ coding: utf-8 ~*~ from django.core.cache import cache +from django.db.models import CharField from django.utils.translation import ugettext as _ from rest_framework import generics from rest_framework.response import Response from rest_framework_bulk import BulkModelViewSet +from common.db.aggregates import GroupConcat from common.permissions import ( IsOrgAdmin, IsOrgAdminOrAppUser, CanUpdateDeleteUser, IsSuperUser @@ -13,6 +15,7 @@ from common.permissions import ( from common.mixins import CommonApiMixin from common.utils import get_logger from orgs.utils import current_org +from orgs.models import ROLE as ORG_ROLE, OrganizationMember from .. import serializers from ..serializers import UserSerializer, UserRetrieveSerializer from .mixins import UserQuerysetMixin @@ -39,7 +42,11 @@ class UserViewSet(CommonApiMixin, UserQuerysetMixin, BulkModelViewSet): extra_filter_backends = [OrgRoleUserFilterBackend] def get_queryset(self): - return super().get_queryset().prefetch_related('groups') + return super().get_queryset().annotate( + gc_m2m_org_members__role=GroupConcat('m2m_org_members__role'), + gc_groups__name=GroupConcat('groups__name'), + gc_groups=GroupConcat('groups__id', output_field=CharField()) + ) def send_created_signal(self, users): if not isinstance(users, list): @@ -48,11 +55,32 @@ class UserViewSet(CommonApiMixin, UserQuerysetMixin, BulkModelViewSet): post_user_create.send(self.__class__, user=user) def perform_create(self, serializer): + validated_data = serializer.validated_data + if isinstance(validated_data, list): + org_roles = [item.pop('org_role', None) for item in validated_data] + else: + org_roles = [validated_data.pop('org_role', None)] + users = serializer.save() if isinstance(users, User): users = [users] if current_org and current_org.is_real(): - current_org.users.add(*users) + mapper = { + ORG_ROLE.USER: [], + ORG_ROLE.ADMIN: [], + ORG_ROLE.AUDITOR: [] + } + + for user, role in zip(users, org_roles): + if role in mapper: + mapper[role].append(user) + else: + mapper[ORG_ROLE.USER].append(user) + OrganizationMember.objects.set_users_by_role( + current_org, users=mapper[ORG_ROLE.USER], + admins=mapper[ORG_ROLE.ADMIN], + auditors=mapper[ORG_ROLE.AUDITOR] + ) self.send_created_signal(users) def get_permissions(self): @@ -78,7 +106,7 @@ class UserViewSet(CommonApiMixin, UserQuerysetMixin, BulkModelViewSet): users_ids = [ d.get("id") or d.get("pk") for d in serializer.validated_data ] - users = current_org.get_org_members().filter(id__in=users_ids) + users = current_org.get_members().filter(id__in=users_ids) for user in users: self.check_object_permissions(self.request, user) return super().perform_bulk_update(serializer) diff --git a/apps/users/filters.py b/apps/users/filters.py index d12d3234e..0aa46609e 100644 --- a/apps/users/filters.py +++ b/apps/users/filters.py @@ -12,13 +12,13 @@ class OrgRoleUserFilterBackend(filters.BaseFilterBackend): return queryset if org_role == 'admins': - return queryset & (current_org.get_org_admins() | User.objects.filter(role=User.ROLE_ADMIN)) + return queryset & (current_org.admins | User.objects.filter(role=User.ROLE_ADMIN)) elif org_role == 'auditors': - return queryset & current_org.get_org_auditors() + return queryset & current_org.auditors elif org_role == 'users': - return queryset & current_org.get_org_users() + return queryset & current_org.users elif org_role == 'members': - return queryset & current_org.get_org_members() + return queryset & current_org.get_members() def get_schema_fields(self, view): return [ diff --git a/apps/users/forms/user.py b/apps/users/forms/user.py index a58e1fef1..e02aaf17e 100644 --- a/apps/users/forms/user.py +++ b/apps/users/forms/user.py @@ -17,14 +17,14 @@ __all__ = [ class UserCreateUpdateFormMixin(OrgModelForm): - role_choices = ((i, n) for i, n in User.ROLE_CHOICES if i != User.ROLE_APP) + role_choices = ((i, n) for i, n in User.ROLE.choices if i != User.ROLE.APP) password = forms.CharField( label=_('Password'), widget=forms.PasswordInput, max_length=128, strip=False, required=False, ) role = forms.ChoiceField( choices=role_choices, required=True, - initial=User.ROLE_USER, label=_("Role") + initial=User.ROLE.USER, label=_("Role") ) source = forms.ChoiceField( choices=get_source_choices, required=True, diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 89bfe4254..456cd1fbc 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -18,8 +18,10 @@ from django.shortcuts import reverse from common.local import LOCAL_DYNAMIC_SETTINGS from orgs.utils import current_org -from common.utils import signer, date_expired_default, get_logger, lazyproperty +from common.utils import date_expired_default, get_logger, lazyproperty from common import fields +from common.const import choices +from common.db.models import ChoiceSet from ..signals import post_user_change_password @@ -150,45 +152,58 @@ class AuthMixin: class RoleMixin: - ROLE_ADMIN = 'Admin' - ROLE_USER = 'User' - ROLE_APP = 'App' - ROLE_AUDITOR = 'Auditor' + class ROLE(ChoiceSet): + ADMIN = choices.ADMIN, _('Super administrator') + USER = choices.USER, _('User') + AUDITOR = choices.AUDITOR, _('Super auditor') + APP = 'App', _('Application') - ROLE_CHOICES = ( - (ROLE_ADMIN, _('Administrator')), - (ROLE_USER, _('User')), - (ROLE_APP, _('Application')), - (ROLE_AUDITOR, _("Auditor")) - ) - role = ROLE_USER + role = ROLE.USER @property - def role_display(self): + def super_role_display(self): + return self.get_role_display() + + @property + def org_role_display(self): + from orgs.models import ROLE as ORG_ROLE + if not current_org.is_real(): - return self.get_role_display() - roles = [] - if self in current_org.get_org_admins(): - roles.append(str(_('Org admin'))) - if self in current_org.get_org_auditors(): - roles.append(str(_('Org auditor'))) - if self in current_org.get_org_users(): - roles.append(str(_('User'))) - return " | ".join(roles) + if self.is_superuser: + return ORG_ROLE.ADMIN.label + else: + return ORG_ROLE.USER.label + + if hasattr(self, 'gc_m2m_org_members__role'): + names = self.gc_m2m_org_members__role + if isinstance(names, str): + roles = set(self.gc_m2m_org_members__role.split(',')) + else: + roles = set() + else: + roles = set(self.m2m_org_members.filter( + org_id=current_org.id + ).values_list('role', flat=True)) + + return ' | '.join([str(ORG_ROLE[role]) for role in roles if role in ORG_ROLE]) def current_org_roles(self): - roles = [] - if self.can_admin_current_org: - roles.append('Admin') - if self.can_audit_current_org: - roles.append('Auditor') - else: - roles.append('User') + from orgs.models import OrganizationMember, ROLE as ORG_ROLE + if not current_org.is_real(): + if self.is_superuser: + return [ORG_ROLE.ADMIN] + else: + return [ORG_ROLE.USER] + + roles = list(set(OrganizationMember.objects.filter( + org_id=current_org.id, user=self + ).values_list('role', flat=True))) + return roles @property def is_superuser(self): - if self.role == 'Admin': + if self.role == self.ROLE.ADMIN: return True else: return False @@ -196,13 +211,13 @@ class RoleMixin: @is_superuser.setter def is_superuser(self, value): if value is True: - self.role = 'Admin' + self.role = self.ROLE.ADMIN else: - self.role = 'User' + self.role = self.ROLE.USER @property def is_super_auditor(self): - return self.role == 'Auditor' + return self.role == self.ROLE.AUDITOR @property def is_common_user(self): @@ -216,7 +231,7 @@ class RoleMixin: @property def is_app(self): - return self.role == 'App' + return self.role == self.ROLE.APP @lazyproperty def user_orgs(self): @@ -240,14 +255,16 @@ class RoleMixin: @lazyproperty def is_org_admin(self): - if self.is_superuser or self.related_admin_orgs.exists(): + from orgs.models import ROLE as ORG_ROLE + if self.is_superuser or self.m2m_org_members.filter(role=ORG_ROLE.ADMIN).exists(): return True else: return False @lazyproperty def is_org_auditor(self): - if self.is_super_auditor or self.related_audit_orgs.exists(): + from orgs.models import ROLE as ORG_ROLE + if self.is_super_auditor or self.m2m_org_members.filter(role=ORG_ROLE.AUDITOR).exists(): return True else: return False @@ -283,7 +300,7 @@ class RoleMixin: def create_app_user(cls, name, comment): app = cls.objects.create( username=name, name=name, email='{}@local.domain'.format(name), - is_active=False, role='App', comment=comment, + is_active=False, role=cls.ROLE.APP, comment=comment, is_first_login=False, created_by='System' ) access_key = app.create_access_key() @@ -473,7 +490,7 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): blank=True, verbose_name=_('User group') ) role = models.CharField( - choices=RoleMixin.ROLE_CHOICES, default='User', max_length=10, + choices=RoleMixin.ROLE.choices, default='User', max_length=10, blank=True, verbose_name=_('Role') ) avatar = models.ImageField( @@ -526,6 +543,12 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): @property def groups_display(self): + if hasattr(self, 'gc_groups__name'): + names = self.gc_groups__name + if isinstance(names, str): + return ' '.join(set(self.gc_groups__name.split(','))) + else: + return '' return ' '.join([group.name for group in self.groups.all()]) @property @@ -646,7 +669,7 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): email=forgery_py.internet.email_address(), name=forgery_py.name.full_name(), password=make_password(forgery_py.lorem_ipsum.word()), - role=choice(list(dict(User.ROLE_CHOICES).keys())), + role=choice(list(dict(User.ROLE.choices).keys())), wechat=forgery_py.internet.user_name(True), comment=forgery_py.lorem_ipsum.sentence(), created_by=choice(cls.objects.all()).username) diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py index bebc5042b..76485eb93 100644 --- a/apps/users/serializers/user.py +++ b/apps/users/serializers/user.py @@ -9,7 +9,8 @@ from common.utils import validate_ssh_public_key from common.mixins import CommonBulkSerializerMixin from common.serializers import AdaptedBulkListSerializer from common.permissions import CanUpdateDeleteUser -from ..models import User +from common.drf.fields import GroupConcatedPrimaryKeyRelatedField +from ..models import User, UserGroup __all__ = [ @@ -38,10 +39,16 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): label=_('Password strategy'), write_only=True ) mfa_level_display = serializers.ReadOnlyField(source='get_mfa_level_display') + groups = GroupConcatedPrimaryKeyRelatedField( + label=_('User group'), many=True, queryset=UserGroup.objects.all(), required=False + ) login_blocked = serializers.SerializerMethodField() can_update = serializers.SerializerMethodField() can_delete = serializers.SerializerMethodField() - + org_role = serializers.CharField( + label=_('Organization role name'), write_only=True, + allow_null=True, required=False, allow_blank=True + ) key_prefix_block = "_LOGIN_BLOCK_{}" class Meta: @@ -52,7 +59,7 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): # small 指的是 不需要计算的直接能从一张表中获取到的数据 fields_small = fields_mini + [ 'password', 'email', 'public_key', 'wechat', 'phone', 'mfa_level', 'mfa_enabled', - 'mfa_level_display', 'mfa_force_enabled', + 'mfa_level_display', 'mfa_force_enabled', 'super_role_display', 'comment', 'source', 'is_valid', 'is_expired', 'is_active', 'created_by', 'is_first_login', 'password_strategy', 'date_password_last_updated', 'date_expired', @@ -60,7 +67,7 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): ] fields = fields_small + [ 'groups', 'role', 'groups_display', 'role_display', - 'can_update', 'can_delete', 'login_blocked', + 'can_update', 'can_delete', 'login_blocked', 'org_role' ] extra_kwargs = { @@ -75,7 +82,8 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): 'can_delete': {'read_only': True}, 'groups_display': {'label': _('Groups name')}, 'source_display': {'label': _('Source name')}, - 'role_display': {'label': _('Role name')}, + 'role_display': {'label': _('Organization role name'), 'source': 'org_role_display'}, + 'super_role_display': {'label': _('Super role name')}, } def __init__(self, *args, **kwargs): @@ -87,17 +95,17 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): if not role: return choices = role._choices - choices.pop(User.ROLE_APP, None) + choices.pop(User.ROLE.APP, None) request = self.context.get('request') if request and hasattr(request, 'user') and not request.user.is_superuser: - choices.pop(User.ROLE_ADMIN, None) - choices.pop(User.ROLE_AUDITOR, None) + choices.pop(User.ROLE.ADMIN, None) + choices.pop(User.ROLE.AUDITOR, None) role._choices = choices def validate_role(self, value): request = self.context.get('request') - if not request.user.is_superuser and value != User.ROLE_USER: - role_display = dict(User.ROLE_CHOICES)[User.ROLE_USER] + if not request.user.is_superuser and value != User.ROLE.USER: + role_display = User.ROLE.USER.label msg = _("Role limit to {}".format(role_display)) raise serializers.ValidationError(msg) return value @@ -121,7 +129,7 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): role = self.initial_data.get('role') if self.instance: role = role or self.instance.role - if role == User.ROLE_AUDITOR: + if role == User.ROLE.AUDITOR: return [] return groups diff --git a/apps/users/templates/users/user_detail.html b/apps/users/templates/users/user_detail.html index e9bdf8b92..0b624b78a 100644 --- a/apps/users/templates/users/user_detail.html +++ b/apps/users/templates/users/user_detail.html @@ -71,7 +71,7 @@ {% endif %} {% trans 'Role' %}: - {{ object.role_display }} + {{ object.org_role_display }} {% trans 'MFA' %}: diff --git a/apps/users/utils.py b/apps/users/utils.py index af6e0197b..cf6be4c67 100644 --- a/apps/users/utils.py +++ b/apps/users/utils.py @@ -315,7 +315,7 @@ def construct_user_email(username, email): def get_current_org_members(exclude=()): from orgs.utils import current_org - return current_org.get_org_members(exclude=exclude) + return current_org.get_members(exclude=exclude) def get_source_choices():