refactor(orgs): 重构组织表结构

pull/4419/head
xinwen 2020-07-20 10:42:22 +08:00 committed by 老广
parent 1bc913ab13
commit de3865fa1d
27 changed files with 702 additions and 299 deletions

View File

@ -43,7 +43,7 @@ class UserLoginLogViewSet(ListModelMixin, CommonGenericViewSet):
@staticmethod @staticmethod
def get_org_members(): 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 return users
def get_queryset(self): def get_queryset(self):
@ -79,7 +79,7 @@ class PasswordChangeLogViewSet(ListModelMixin, CommonGenericViewSet):
ordering = ['-datetime'] ordering = ['-datetime']
def get_queryset(self): def get_queryset(self):
users = current_org.get_org_members() users = current_org.get_members()
queryset = super().get_queryset().filter( queryset = super().get_queryset().filter(
user__in=[user.__str__() for user in users] user__in=[user.__str__() for user in users]
) )

View File

@ -20,7 +20,7 @@ class CurrentOrgMembersFilter(filters.BaseFilterBackend):
] ]
def _get_user_list(self): def _get_user_list(self):
users = current_org.get_org_members(exclude=('Auditor',)) users = current_org.get_members(exclude=('Auditor',))
return users return users
def filter_queryset(self, request, queryset, view): def filter_queryset(self, request, queryset, view):

View File

@ -124,7 +124,7 @@ class UserLoginLog(models.Model):
Q(username__contains=keyword) Q(username__contains=keyword)
) )
if not current_org.is_root(): 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) login_logs = login_logs.filter(username__in=username_list)
return login_logs return login_logs

View File

@ -0,0 +1,8 @@
from django.utils.translation import ugettext_lazy as _
from common.db.models import ChoiceSet
ADMIN = 'Admin'
USER = 'User'
AUDITOR = 'Auditor'

View File

@ -3,10 +3,10 @@ from django.db.models import Aggregate
class GroupConcat(Aggregate): class GroupConcat(Aggregate):
function = 'GROUP_CONCAT' 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 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 = '' order_by_clause = ''
if order_by is not None: if order_by is not None:
order = 'ASC' order = 'ASC'
@ -21,8 +21,7 @@ class GroupConcat(Aggregate):
super().__init__( super().__init__(
expression, expression,
distinct='DISTINCT' if distinct else '',
order_by=order_by_clause, order_by=order_by_clause,
separator=f'SEPARATOR {separator}', separator=f"SEPARATOR '{separator}'",
**extra **extra
) )

48
apps/common/db/models.py Normal file
View File

@ -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 配置, 为了代码提示在此声明

43
apps/common/drf/fields.py Normal file
View File

@ -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

View File

@ -128,7 +128,7 @@ class DatesLoginMetricMixin:
@lazyproperty @lazyproperty
def dates_total_count_inactive_users(self): 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 active = self.dates_total_count_active_users
count = total - active count = total - active
if count < 0: if count < 0:
@ -137,7 +137,7 @@ class DatesLoginMetricMixin:
@lazyproperty @lazyproperty
def dates_total_count_disabled_users(self): 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 @lazyproperty
def dates_total_count_active_assets(self): def dates_total_count_active_assets(self):
@ -207,7 +207,7 @@ class DatesLoginMetricMixin:
class TotalCountMixin: class TotalCountMixin:
@staticmethod @staticmethod
def get_total_count_users(): def get_total_count_users():
return current_org.get_org_members().count() return current_org.get_members().count()
@staticmethod @staticmethod
def get_total_count_assets(): def get_total_count_assets():

Binary file not shown.

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: JumpServer 0.3.3\n" "Project-Id-Version: JumpServer 0.3.3\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: ibuler <ibuler@qq.com>\n" "Last-Translator: ibuler <ibuler@qq.com>\n"
"Language-Team: JumpServer team<ibuler@qq.com>\n" "Language-Team: JumpServer team<ibuler@qq.com>\n"
@ -25,10 +25,10 @@ msgstr "自定义"
#: assets/models/asset.py:145 assets/models/base.py:232 #: assets/models/asset.py:145 assets/models/base.py:232
#: assets/models/cluster.py:18 assets/models/cmd_filter.py:21 #: assets/models/cluster.py:18 assets/models/cmd_filter.py:21
#: assets/models/domain.py:20 assets/models/group.py:20 #: 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 #: 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 #: 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/_select_user_modal.html:13
#: users/templates/users/user_asset_permission.html:37 #: users/templates/users/user_asset_permission.html:37
#: users/templates/users/user_asset_permission.html:154 #: 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/cmd_filter.py:23 assets/models/cmd_filter.py:57
#: assets/models/domain.py:21 assets/models/domain.py:54 #: 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 #: 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 #: 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_detail.html:115
#: users/templates/users/user_granted_database_app.html:38 #: users/templates/users/user_granted_database_app.html:38
#: users/templates/users/user_granted_remote_app.html:37 #: 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 #: applications/models/remote_app.py:39 assets/models/asset.py:224
#: assets/models/base.py:240 assets/models/cluster.py:28 #: assets/models/base.py:240 assets/models/cluster.py:28
#: assets/models/cmd_filter.py:26 assets/models/cmd_filter.py:60 #: 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 #: assets/models/group.py:21 common/mixins/models.py:49 orgs/models.py:23
#: perms/models/base.py:54 users/models/user.py:508 #: 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 #: 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/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 #: 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/domain.py:23 assets/models/gathered_user.py:19
#: assets/models/group.py:22 assets/models/label.py:25 #: 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 #: 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 #: orgs/models.py:24 orgs/models.py:314 perms/models/base.py:55
#: users/templates/users/user_group_detail.html:58 #: 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 #: xpack/plugins/cloud/models.py:59 xpack/plugins/cloud/models.py:149
msgid "Date created" msgid "Date created"
msgstr "创建日期" msgstr "创建日期"
@ -336,10 +336,10 @@ msgid "AuthBook"
msgstr "" msgstr ""
#: assets/models/base.py:233 assets/models/gathered_user.py:15 #: 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/login.html:21
#: authentication/templates/authentication/xpack_login.html:93 #: authentication/templates/authentication/xpack_login.html:101
#: ops/models/adhoc.py:148 users/forms/profile.py:19 users/models/user.py:465 #: 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/_select_user_modal.html:14
#: users/templates/users/user_detail.html:53 #: users/templates/users/user_detail.html:53
#: users/templates/users/user_list.html:15 #: users/templates/users/user_list.html:15
@ -350,9 +350,9 @@ msgid "Username"
msgstr "用户名" msgstr "用户名"
#: assets/models/base.py:234 assets/serializers/asset_user.py:71 #: 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/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/forms/user.py:22 users/forms/user.py:193
#: users/templates/users/user_otp_check_password.html:13 #: users/templates/users/user_otp_check_password.html:13
#: users/templates/users/user_password_update.html:43 #: users/templates/users/user_password_update.html:43
@ -379,7 +379,7 @@ msgid "SSH public key"
msgstr "SSH公钥" msgstr "SSH公钥"
#: assets/models/base.py:239 assets/models/gathered_user.py:20 #: 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" msgid "Date updated"
msgstr "更新日期" msgstr "更新日期"
@ -391,7 +391,7 @@ msgstr "带宽"
msgid "Contact" msgid "Contact"
msgstr "联系人" 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 #: users/templates/users/user_detail.html:62
msgid "Phone" msgid "Phone"
msgstr "手机" msgstr "手机"
@ -417,7 +417,7 @@ msgid "Default"
msgstr "默认" msgstr "默认"
#: assets/models/cluster.py:36 assets/models/label.py:14 #: assets/models/cluster.py:36 assets/models/label.py:14
#: users/models/user.py:627 #: users/models/user.py:635
msgid "System" msgid "System"
msgstr "系统" msgstr "系统"
@ -535,14 +535,15 @@ msgstr "默认资产组"
#: assets/models/label.py:15 audits/models.py:36 audits/models.py:56 #: 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 #: 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 #: perms/forms/remote_app_permission.py:40 perms/models/base.py:49
#: templates/index.html:78 terminal/backends/command/models.py:18 #: templates/index.html:78 terminal/backends/command/models.py:18
#: terminal/backends/command/serializers.py:12 terminal/models.py:185 #: terminal/backends/command/serializers.py:12 terminal/models.py:185
#: tickets/models/ticket.py:35 tickets/models/ticket.py:130 #: tickets/models/ticket.py:35 tickets/models/ticket.py:130
#: tickets/serializers/request_asset_perm.py:55 #: tickets/serializers/request_asset_perm.py:55
#: tickets/serializers/ticket.py:27 users/forms/group.py:15 #: 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/serializers/group.py:20
#: users/templates/users/user_asset_permission.html:38 #: users/templates/users/user_asset_permission.html:38
#: users/templates/users/user_asset_permission.html:64 #: users/templates/users/user_asset_permission.html:64
@ -707,14 +708,14 @@ msgid "Backend"
msgstr "后端" msgstr "后端"
#: assets/serializers/asset_user.py:75 users/forms/profile.py:148 #: 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.html:69
#: users/templates/users/user_profile_update.html:46 #: users/templates/users/user_profile_update.html:46
#: users/templates/users/user_pubkey_update.html:46 #: users/templates/users/user_pubkey_update.html:46
msgid "Public key" msgid "Public key"
msgstr "SSH公钥" 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" msgid "Private key"
msgstr "ssh私钥" msgstr "ssh私钥"
@ -999,8 +1000,8 @@ msgstr "Agent"
#: audits/models.py:104 #: audits/models.py:104
#: authentication/templates/authentication/_mfa_confirm_modal.html:14 #: authentication/templates/authentication/_mfa_confirm_modal.html:14
#: authentication/templates/authentication/login_otp.html:6 #: authentication/templates/authentication/login_otp.html:6
#: users/forms/profile.py:52 users/models/user.py:489 #: users/forms/profile.py:52 users/models/user.py:495
#: users/serializers/user.py:220 users/templates/users/user_detail.html:77 #: users/serializers/user.py:224 users/templates/users/user_detail.html:77
#: users/templates/users/user_profile.html:87 #: users/templates/users/user_profile.html:87
msgid "MFA" msgid "MFA"
msgstr "多因子认证" msgstr "多因子认证"
@ -1169,7 +1170,10 @@ msgstr "等待登录复核处理"
msgid "Login confirm ticket was {}" msgid "Login confirm ticket was {}"
msgstr "登录复核 {}" 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" msgid "MFA code"
msgstr "多因子认证验证码" msgstr "多因子认证验证码"
@ -1224,7 +1228,7 @@ msgid "Show"
msgstr "显示" msgstr "显示"
#: authentication/templates/authentication/_access_key_modal.html:66 #: 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:94
#: users/templates/users/user_profile.html:163 #: users/templates/users/user_profile.html:163
#: users/templates/users/user_profile.html:166 #: users/templates/users/user_profile.html:166
@ -1233,7 +1237,7 @@ msgid "Disable"
msgstr "禁用" msgstr "禁用"
#: authentication/templates/authentication/_access_key_modal.html:67 #: 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:92
#: users/templates/users/user_profile.html:170 #: users/templates/users/user_profile.html:170
msgid "Enable" msgid "Enable"
@ -1274,29 +1278,29 @@ msgid "Code error"
msgstr "代码错误" msgstr "代码错误"
#: authentication/templates/authentication/login.html:6 #: authentication/templates/authentication/login.html:6
#: authentication/templates/authentication/login.html:39 #: authentication/templates/authentication/login.html:49
#: authentication/templates/authentication/xpack_login.html:112 #: authentication/templates/authentication/xpack_login.html:130
#: templates/_base_only_msg_content.html:51 templates/_header_bar.html:83 #: templates/_base_only_msg_content.html:51 templates/_header_bar.html:83
msgid "Login" msgid "Login"
msgstr "登录" msgstr "登录"
#: authentication/templates/authentication/login.html:17 #: authentication/templates/authentication/login.html:17
#: authentication/templates/authentication/xpack_login.html:87 #: authentication/templates/authentication/xpack_login.html:95
msgid "Captcha invalid" msgid "Captcha invalid"
msgstr "验证码错误" msgstr "验证码错误"
#: authentication/templates/authentication/login.html:50 #: authentication/templates/authentication/login.html:60
#: authentication/templates/authentication/xpack_login.html:116 #: authentication/templates/authentication/xpack_login.html:134
#: users/templates/users/forgot_password.html:7 #: users/templates/users/forgot_password.html:7
#: users/templates/users/forgot_password.html:8 #: users/templates/users/forgot_password.html:8
msgid "Forgot password" msgid "Forgot password"
msgstr "忘记密码" msgstr "忘记密码"
#: authentication/templates/authentication/login.html:57 #: authentication/templates/authentication/login.html:67
msgid "More login options" msgid "More login options"
msgstr "更多登录方式" msgstr "更多登录方式"
#: authentication/templates/authentication/login.html:61 #: authentication/templates/authentication/login.html:71
msgid "OpenID" msgid "OpenID"
msgstr "OpenID" msgstr "OpenID"
@ -1337,11 +1341,11 @@ msgstr "返回"
msgid "Copy success" msgid "Copy success"
msgstr "复制成功" 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" msgid "Welcome back, please enter username and password to login"
msgstr "欢迎回来,请输入用户名和密码登录" msgstr "欢迎回来,请输入用户名和密码登录"
#: authentication/views/login.py:83 #: authentication/views/login.py:82
msgid "Please enable cookies and try again." msgid "Please enable cookies and try again."
msgstr "设置你的浏览器支持cookie" msgstr "设置你的浏览器支持cookie"
@ -1602,11 +1606,11 @@ msgstr "命令 `{}` 不允许被执行 ......."
msgid "Task end" msgid "Task end"
msgstr "任务结束" msgstr "任务结束"
#: ops/tasks.py:68 #: ops/tasks.py:71
msgid "Clean task history period" msgid "Clean task history period"
msgstr "定期清除任务历史" msgstr "定期清除任务历史"
#: ops/tasks.py:81 #: ops/tasks.py:84
msgid "Clean celery log period" msgid "Clean celery log period"
msgstr "定期清除Celery日志" msgstr "定期清除Celery日志"
@ -1622,18 +1626,35 @@ msgstr "更新任务内容: {}"
msgid "Disk used more than 80%: {} => {}" msgid "Disk used more than 80%: {} => {}"
msgstr "磁盘使用率超过 80%: {} => {}" msgstr "磁盘使用率超过 80%: {} => {}"
#: orgs/api.py:57 #: orgs/api.py:54
msgid "Organization contains undeleted resources" msgid "Organization contains undeleted resources"
msgstr "组织内包含未删除的资源" msgstr "组织内包含未删除的资源"
#: orgs/api.py:61 #: orgs/api.py:58
msgid "The current organization cannot be deleted" msgid "The current organization cannot be deleted"
msgstr "当能删除当前所在组织" 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" msgid "Organization"
msgstr "组织" 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 #: perms/const.py:7
msgid "Ungrouped" msgid "Ungrouped"
msgstr "未分组" msgstr "未分组"
@ -1651,7 +1672,8 @@ msgstr "提示RDP 协议不支持单独控制上传或下载文件"
#: perms/forms/asset_permission.py:86 perms/forms/database_app_permission.py:41 #: 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 #: 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 #: 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:39
#: users/templates/users/user_asset_permission.html:67 #: users/templates/users/user_asset_permission.html:67
#: users/templates/users/user_database_app_permission.html:38 #: users/templates/users/user_database_app_permission.html:38
@ -1717,7 +1739,7 @@ msgid "Asset permission"
msgstr "资产授权" msgstr "资产授权"
#: perms/models/base.py:53 tickets/serializers/request_asset_perm.py:20 #: 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 #: users/templates/users/user_profile.html:120
msgid "Date expired" msgid "Date expired"
msgstr "失效日期" msgstr "失效日期"
@ -2634,7 +2656,7 @@ msgstr ""
" </div>\n" " </div>\n"
" " " "
#: users/api/user.py:119 #: users/api/user.py:126
msgid "Could not reset self otp, use profile reset instead" msgid "Could not reset self otp, use profile reset instead"
msgstr "不能在该页面重置多因子认证, 请去个人信息页面重置" msgstr "不能在该页面重置多因子认证, 请去个人信息页面重置"
@ -2680,7 +2702,7 @@ msgstr "确认密码"
msgid "Password does not match" msgid "Password does not match"
msgstr "密码不一致" 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_detail.html:57
#: users/templates/users/user_profile.html:59 #: users/templates/users/user_profile.html:59
msgid "Email" msgid "Email"
@ -2716,20 +2738,12 @@ msgid "Public key should not be the same as your old one."
msgstr "不能和原来的密钥相同" msgstr "不能和原来的密钥相同"
#: users/forms/profile.py:137 users/forms/user.py:90 #: users/forms/profile.py:137 users/forms/user.py:90
#: users/serializers/user.py:181 users/serializers/user.py:262 #: users/serializers/user.py:185 users/serializers/user.py:266
#: users/serializers/user.py:320 #: users/serializers/user.py:324
msgid "Not a valid ssh public key" msgid "Not a valid ssh public key"
msgstr "SSH密钥不合法" msgstr "SSH密钥不合法"
#: users/forms/user.py:27 users/models/user.py:477 #: users/forms/user.py:31 users/models/user.py:518
#: 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/templates/users/user_detail.html:89 #: users/templates/users/user_detail.html:89
#: users/templates/users/user_list.html:18 #: users/templates/users/user_list.html:18
#: users/templates/users/user_profile.html:102 #: users/templates/users/user_profile.html:102
@ -2749,105 +2763,105 @@ msgstr "添加到用户组"
msgid "* Your password does not meet the requirements" msgid "* Your password does not meet the requirements"
msgstr "* 您的密码不符合要求" 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" msgid "Reset link will be generated and sent to the user"
msgstr "生成重置密码链接,通过邮件发送给用户" msgstr "生成重置密码链接,通过邮件发送给用户"
#: users/forms/user.py:125 users/serializers/user.py:31 #: users/forms/user.py:125 users/serializers/user.py:32
msgid "Set password" msgid "Set password"
msgstr "设置密码" 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/models.py:61
#: xpack/plugins/change_auth_plan/serializers.py:30 #: xpack/plugins/change_auth_plan/serializers.py:30
msgid "Password strategy" msgid "Password strategy"
msgstr "密码策略" msgstr "密码策略"
#: users/models/user.py:159 users/models/user.py:623 #: users/models/user.py:156
msgid "Administrator" msgid "Super administrator"
msgstr "管理员" msgstr "超级管理员"
#: users/models/user.py:161 #: users/models/user.py:158
msgid "Super auditor"
msgstr "超级审计员"
#: users/models/user.py:159
msgid "Application" msgid "Application"
msgstr "应用程序" msgstr "应用程序"
#: users/models/user.py:162 #: users/models/user.py:395 users/templates/users/user_profile.html:90
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
msgid "Force enable" msgid "Force enable"
msgstr "强制启用" msgstr "强制启用"
#: users/models/user.py:456 #: users/models/user.py:462
msgid "Local" msgid "Local"
msgstr "数据库" msgstr "数据库"
#: users/models/user.py:480 #: users/models/user.py:486
msgid "Avatar" msgid "Avatar"
msgstr "头像" 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" msgid "Wechat"
msgstr "微信" msgstr "微信"
#: users/models/user.py:516 #: users/models/user.py:522
msgid "Date password last updated" msgid "Date password last updated"
msgstr "最后更新密码日期" 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" msgid "Administrator is the super user of system"
msgstr "Administrator是初始的超级管理员" 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" msgid "Is first login"
msgstr "首次登录" msgstr "首次登录"
#: users/serializers/user.py:70 #: users/serializers/user.py:73
msgid "Is valid" msgid "Is valid"
msgstr "账户是否有效" msgstr "账户是否有效"
#: users/serializers/user.py:71 #: users/serializers/user.py:74
msgid "Is expired" msgid "Is expired"
msgstr " 是否过期" msgstr " 是否过期"
#: users/serializers/user.py:72 #: users/serializers/user.py:75
msgid "Avatar url" msgid "Avatar url"
msgstr "头像路径" msgstr "头像路径"
#: users/serializers/user.py:76 #: users/serializers/user.py:79
msgid "Groups name" msgid "Groups name"
msgstr "用户组名" msgstr "用户组名"
#: users/serializers/user.py:77 #: users/serializers/user.py:80
msgid "Source name" msgid "Source name"
msgstr "用户来源名" msgstr "用户来源名"
#: users/serializers/user.py:78 #: users/serializers/user.py:81
msgid "Role name" msgid "Organization role name"
msgstr "角色名" 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 {}" msgid "Role limit to {}"
msgstr "角色只能为 {}" 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" msgid "Password does not match security rules"
msgstr "密码不满足安全规则" msgstr "密码不满足安全规则"
#: users/serializers/user.py:278 #: users/serializers/user.py:282
msgid "The old password is incorrect" msgid "The old password is incorrect"
msgstr "旧密码错误" msgstr "旧密码错误"
#: users/serializers/user.py:292 #: users/serializers/user.py:296
msgid "The newly set password is inconsistent" msgid "The newly set password is inconsistent"
msgstr "两次密码不一致" msgstr "两次密码不一致"
@ -3969,6 +3983,15 @@ msgstr "企业版"
msgid "Ultimate edition" msgid "Ultimate edition"
msgstr "旗舰版" msgstr "旗舰版"
#~ msgid "Auditor"
#~ msgstr "审计员"
#~ msgid "Org admin"
#~ msgstr "组织管理员"
#~ msgid "Role name"
#~ msgstr "角色名"
#~ msgid "GUI copy" #~ msgid "GUI copy"
#~ msgstr "GUI 复制" #~ msgstr "GUI 复制"
@ -5508,9 +5531,6 @@ msgstr "旗舰版"
#~ msgid "Admin" #~ msgid "Admin"
#~ msgstr "管理员" #~ msgstr "管理员"
#~ msgid "Organizations"
#~ msgstr "组织管理"
#~ msgid "Org detail" #~ msgid "Org detail"
#~ msgstr "组织详情" #~ msgstr "组织详情"

View File

@ -1,23 +1,20 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from rest_framework import status, generics from rest_framework import status, generics
from rest_framework.views import Response from rest_framework.views import Response
from rest_framework_bulk import BulkModelViewSet from rest_framework_bulk import BulkModelViewSet
from common.permissions import IsSuperUserOrAppUser from common.permissions import IsSuperUserOrAppUser
from .models import Organization from .models import Organization, ROLE
from .serializers import OrgSerializer, OrgReadSerializer, \ from .serializers import OrgSerializer, OrgReadSerializer, \
OrgMembershipUserSerializer, OrgMembershipAdminSerializer, \
OrgAllUserSerializer, OrgRetrieveSerializer OrgAllUserSerializer, OrgRetrieveSerializer
from users.models import User, UserGroup from users.models import User, UserGroup
from assets.models import Asset, Domain, AdminUser, SystemUser, Label from assets.models import Asset, Domain, AdminUser, SystemUser, Label
from perms.models import AssetPermission from perms.models import AssetPermission
from orgs.utils import current_org from orgs.utils import current_org
from common.utils import get_logger from common.utils import get_logger
from .mixins.api import OrgMembershipModelViewSetMixin
logger = get_logger(__file__) logger = get_logger(__file__)
@ -39,7 +36,7 @@ class OrgViewSet(BulkModelViewSet):
def get_data_from_model(self, model): def get_data_from_model(self, model):
if model == User: 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: else:
data = model.objects.filter(org_id=self.org.id) data = model.objects.filter(org_id=self.org.id)
return data return data
@ -64,18 +61,6 @@ class OrgViewSet(BulkModelViewSet):
return Response({'msg': True}, status=status.HTTP_200_OK) 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): class OrgAllUserListApi(generics.ListAPIView):
permission_classes = (IsSuperUserOrAppUser,) permission_classes = (IsSuperUserOrAppUser,)
serializer_class = OrgAllUserSerializer serializer_class = OrgAllUserSerializer
@ -84,6 +69,7 @@ class OrgAllUserListApi(generics.ListAPIView):
def get_queryset(self): def get_queryset(self):
pk = self.kwargs.get("pk") pk = self.kwargs.get("pk")
org = get_object_or_404(Organization, pk=pk) users = User.objects.filter(
users = org.get_org_users().only(*self.serializer_class.Meta.only_fields) orgs=pk, m2m_org_members__role=ROLE.USER
).only(*self.serializer_class.Meta.only_fields)
return users return users

View File

@ -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')},
},
),
]

View File

@ -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)
]

View File

@ -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),
),
]

View File

@ -1,21 +1,30 @@
import uuid import uuid
from django.conf import settings from functools import partial
from django.db import models 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 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): class Organization(models.Model):
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=128, unique=True, verbose_name=_("Name")) 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')) 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')) 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')) 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 orgs = None
CACHE_PREFIX = 'JMS_ORG_{}' CACHE_PREFIX = 'JMS_ORG_{}'
@ -72,29 +81,24 @@ class Organization(models.Model):
org = cls.default() if default else None org = cls.default() if default else None
return org return org
# @lazyproperty def get_org_members_by_role(self, role):
# lazyproperty 导致用户列表中角色显示出现不稳定的情况, 如果不加会导致数据库操作次数太多
def org_users(self):
from users.models import User from users.models import User
if self.is_real(): if self.is_real():
return self.users.all() return self.members.filter(m2m_org_members__role=role)
users = User.objects.filter(role=User.ROLE_USER) users = User.objects.filter(role=role)
if self.is_default() and not settings.DEFAULT_ORG_SHOW_ALL_USERS:
users = users.filter(related_user_orgs__isnull=True)
return users return users
def get_org_users(self): @property
return self.org_users() def users(self):
return self.get_org_members_by_role(ROLE.USER)
# @lazyproperty @property
def org_admins(self): def admins(self):
from users.models import User return self.get_org_members_by_role(ROLE.ADMIN)
if self.is_real():
return self.admins.all()
return User.objects.filter(role=User.ROLE_ADMIN)
def get_org_admins(self): @property
return self.org_admins() def auditors(self):
return self.get_org_members_by_role(ROLE.AUDITOR)
def org_id(self): def org_id(self):
if self.is_real(): if self.is_real():
@ -104,87 +108,76 @@ class Organization(models.Model):
else: else:
return '' return ''
# @lazyproperty def get_members(self, exclude=()):
def org_auditors(self):
from users.models import User from users.models import User
if self.is_real(): if self.is_real():
return self.auditors.all() members = self.members.exclude(m2m_org_members__role__in=exclude)
return User.objects.filter(role=User.ROLE_AUDITOR) else:
members = User.objects.exclude(role__in=exclude)
def get_org_auditors(self): return members.exclude(role=User.ROLE.APP).distinct()
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()
def can_admin_by(self, user): def can_admin_by(self, user):
if user.is_superuser: if user.is_superuser:
return True return True
if self.get_org_admins().filter(id=user.id): if self.admins.filter(id=user.id).exists():
return True return True
return False return False
def can_audit_by(self, user): def can_audit_by(self, user):
if user.is_super_auditor: if user.is_super_auditor:
return True return True
if self.get_org_auditors().filter(id=user.id): if self.auditors.filter(id=user.id).exists():
return True return True
return False return False
def can_user_by(self, user): 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 True
return False return False
def is_real(self): def is_real(self):
return self.id not in (self.DEFAULT_NAME, self.ROOT_ID, self.SYSTEM_ID) 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 @classmethod
def get_user_admin_orgs(cls, user): def get_user_admin_orgs(cls, user):
admin_orgs = []
if user.is_anonymous: if user.is_anonymous:
return admin_orgs return cls.objects.none()
elif user.is_superuser: if user.is_superuser:
admin_orgs = list(cls.objects.all()) return [*cls.objects.all(), cls.default()]
admin_orgs.append(cls.default()) return cls.get_user_orgs_by_role(user, ROLE.ADMIN)
elif user.is_org_admin:
admin_orgs = user.related_admin_orgs.all()
return admin_orgs
@classmethod @classmethod
def get_user_user_orgs(cls, user): def get_user_user_orgs(cls, user):
user_orgs = []
if user.is_anonymous: if user.is_anonymous:
return user_orgs return cls.objects.none()
user_orgs = user.related_user_orgs.all() return cls.get_user_orgs_by_role(user, ROLE.USER)
return user_orgs
@classmethod @classmethod
def get_user_audit_orgs(cls, user): def get_user_audit_orgs(cls, user):
audit_orgs = []
if user.is_anonymous: if user.is_anonymous:
return audit_orgs return cls.objects.none()
elif user.is_super_auditor: if user.is_super_auditor:
audit_orgs = list(cls.objects.all()) return [*cls.objects.all(), cls.default()]
audit_orgs.append(cls.default()) return cls.get_user_orgs_by_role(user, ROLE.AUDITOR)
elif user.is_org_auditor:
audit_orgs = user.related_audit_orgs.all()
return audit_orgs
@classmethod @classmethod
def get_user_admin_or_audit_orgs(self, user): def get_user_admin_or_audit_orgs(cls, user):
admin_orgs = self.get_user_admin_orgs(user) if user.is_anonymous:
audit_orgs = self.get_user_audit_orgs(user) return cls.objects.none()
orgs = set(admin_orgs) | set(audit_orgs) if user.is_superuser or user.is_super_auditor:
return orgs return [*cls.objects.all(), cls.default()]
return cls.get_user_orgs_by_role(user, (ROLE.AUDITOR, ROLE.ADMIN))
@classmethod @classmethod
def default(cls): def default(cls):
@ -211,8 +204,122 @@ class Organization(models.Model):
from .utils import set_current_org from .utils import set_current_org
set_current_org(self) set_current_org(self)
@classmethod
def all_orgs(cls): def _convert_to_uuid_set(users):
orgs = list(cls.objects.all()) rst = set()
orgs.append(cls.default()) for user in users:
return orgs 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)

View File

@ -1,16 +1,18 @@
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer
from rest_framework import serializers from rest_framework import serializers
from users.models import UserGroup
from assets.models import Asset, Domain, AdminUser, SystemUser, Label from users.models.user import User
from perms.models import AssetPermission
from common.serializers import AdaptedBulkListSerializer from common.serializers import AdaptedBulkListSerializer
from .utils import set_current_org, get_current_org from .models import Organization, OrganizationMember
from .models import Organization
from .mixins.serializers import OrgMembershipSerializerMixin from .mixins.serializers import OrgMembershipSerializerMixin
class OrgSerializer(ModelSerializer): 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: class Meta:
model = Organization model = Organization
list_serializer_class = AdaptedBulkListSerializer list_serializer_class = AdaptedBulkListSerializer
@ -21,11 +23,27 @@ class OrgSerializer(ModelSerializer):
fields_m2m = ['users', 'admins', 'auditors'] fields_m2m = ['users', 'admins', 'auditors']
fields = fields_small + fields_m2m fields = fields_small + fields_m2m
read_only_fields = ['created_by', 'date_created'] read_only_fields = ['created_by', 'date_created']
extra_kwargs = {
'admins': {'write_only': True}, def create(self, validated_data):
'users': {'write_only': True}, members = self._pop_memebers(validated_data)
'auditors': {'write_only': True}, 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): class OrgReadSerializer(OrgSerializer):
@ -34,14 +52,14 @@ class OrgReadSerializer(OrgSerializer):
class OrgMembershipAdminSerializer(OrgMembershipSerializerMixin, ModelSerializer): class OrgMembershipAdminSerializer(OrgMembershipSerializerMixin, ModelSerializer):
class Meta: class Meta:
model = Organization.admins.through model = Organization.members.through
list_serializer_class = AdaptedBulkListSerializer list_serializer_class = AdaptedBulkListSerializer
fields = '__all__' fields = '__all__'
class OrgMembershipUserSerializer(OrgMembershipSerializerMixin, ModelSerializer): class OrgMembershipUserSerializer(OrgMembershipSerializerMixin, ModelSerializer):
class Meta: class Meta:
model = Organization.users.through model = Organization.members.through
list_serializer_class = AdaptedBulkListSerializer list_serializer_class = AdaptedBulkListSerializer
fields = '__all__' fields = '__all__'

View File

@ -5,7 +5,7 @@ from django.db.models.signals import m2m_changed
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver 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 .hands import set_current_org, current_org, Node, get_current_org
from perms.models import AssetPermission from perms.models import AssetPermission
from users.models import UserGroup from users.models import UserGroup
@ -26,23 +26,31 @@ def on_org_create_or_update(sender, instance=None, created=False, **kwargs):
instance.expire_cache() instance.expire_cache()
@receiver(m2m_changed, sender=Organization.users.through) def _remove_users(model, users, org, reverse=False):
def on_org_user_changed(sender, instance=None, **kwargs): if not isinstance(users, (tuple, list, set)):
if isinstance(instance, Organization): users = (users, )
old_org = current_org
set_current_org(instance) m2m_model = model.users.through
if kwargs['action'] == 'pre_remove': if reverse:
users = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) m2m_field_name = model.users.field.m2m_reverse_field_name()
for user in users: else:
perms = AssetPermission.objects.filter(users=user) m2m_field_name = model.users.field.m2m_field_name()
user_groups = UserGroup.objects.filter(users=user) m2m_model.objects.filter(**{'user__in': users, f'{m2m_field_name}__org_id': org.id}).delete()
for perm in perms:
perm.users.remove(user)
for user_group in user_groups:
user_group.users.remove(user)
set_current_org(old_org)
@receiver(m2m_changed, sender=Organization.admins.through) def _clear_users_from_org(org, users):
def on_org_admin_change(sender, **kwargs): if not users:
Organization._user_admin_orgs = None 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)

View File

@ -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.

View File

@ -11,12 +11,6 @@ from .. import api
app_name = 'orgs' app_name = 'orgs'
router = DefaultRouter() router = DefaultRouter()
# 将会删除
router.register(r'orgs/(?P<org_id>[0-9a-zA-Z\-]{36})/membership/admins',
api.OrgMembershipAdminsViewSet, 'membership-admins')
router.register(r'orgs/(?P<org_id>[0-9a-zA-Z\-]{36})/membership/users',
api.OrgMembershipUsersViewSet, 'membership-users'),
router.register(r'orgs', api.OrgViewSet, 'org') router.register(r'orgs', api.OrgViewSet, 'org')
old_version_urlpatterns = [ old_version_urlpatterns = [

View File

@ -9,7 +9,7 @@ from orgs.utils import current_org
class UserQuerysetMixin: class UserQuerysetMixin:
def get_queryset(self): def get_queryset(self):
if self.request.query_params.get('all') or not current_org.is_real(): 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: else:
queryset = utils.get_current_org_members() queryset = utils.get_current_org_members()
return queryset return queryset

View File

@ -1,11 +1,13 @@
# ~*~ coding: utf-8 ~*~ # ~*~ coding: utf-8 ~*~
from django.core.cache import cache from django.core.cache import cache
from django.db.models import CharField
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from rest_framework import generics from rest_framework import generics
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework_bulk import BulkModelViewSet from rest_framework_bulk import BulkModelViewSet
from common.db.aggregates import GroupConcat
from common.permissions import ( from common.permissions import (
IsOrgAdmin, IsOrgAdminOrAppUser, IsOrgAdmin, IsOrgAdminOrAppUser,
CanUpdateDeleteUser, IsSuperUser CanUpdateDeleteUser, IsSuperUser
@ -13,6 +15,7 @@ from common.permissions import (
from common.mixins import CommonApiMixin from common.mixins import CommonApiMixin
from common.utils import get_logger from common.utils import get_logger
from orgs.utils import current_org from orgs.utils import current_org
from orgs.models import ROLE as ORG_ROLE, OrganizationMember
from .. import serializers from .. import serializers
from ..serializers import UserSerializer, UserRetrieveSerializer from ..serializers import UserSerializer, UserRetrieveSerializer
from .mixins import UserQuerysetMixin from .mixins import UserQuerysetMixin
@ -39,7 +42,11 @@ class UserViewSet(CommonApiMixin, UserQuerysetMixin, BulkModelViewSet):
extra_filter_backends = [OrgRoleUserFilterBackend] extra_filter_backends = [OrgRoleUserFilterBackend]
def get_queryset(self): 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): def send_created_signal(self, users):
if not isinstance(users, list): if not isinstance(users, list):
@ -48,11 +55,32 @@ class UserViewSet(CommonApiMixin, UserQuerysetMixin, BulkModelViewSet):
post_user_create.send(self.__class__, user=user) post_user_create.send(self.__class__, user=user)
def perform_create(self, serializer): 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() users = serializer.save()
if isinstance(users, User): if isinstance(users, User):
users = [users] users = [users]
if current_org and current_org.is_real(): 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) self.send_created_signal(users)
def get_permissions(self): def get_permissions(self):
@ -78,7 +106,7 @@ class UserViewSet(CommonApiMixin, UserQuerysetMixin, BulkModelViewSet):
users_ids = [ users_ids = [
d.get("id") or d.get("pk") for d in serializer.validated_data 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: for user in users:
self.check_object_permissions(self.request, user) self.check_object_permissions(self.request, user)
return super().perform_bulk_update(serializer) return super().perform_bulk_update(serializer)

View File

@ -12,13 +12,13 @@ class OrgRoleUserFilterBackend(filters.BaseFilterBackend):
return queryset return queryset
if org_role == 'admins': 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': elif org_role == 'auditors':
return queryset & current_org.get_org_auditors() return queryset & current_org.auditors
elif org_role == 'users': elif org_role == 'users':
return queryset & current_org.get_org_users() return queryset & current_org.users
elif org_role == 'members': elif org_role == 'members':
return queryset & current_org.get_org_members() return queryset & current_org.get_members()
def get_schema_fields(self, view): def get_schema_fields(self, view):
return [ return [

View File

@ -17,14 +17,14 @@ __all__ = [
class UserCreateUpdateFormMixin(OrgModelForm): 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( password = forms.CharField(
label=_('Password'), widget=forms.PasswordInput, label=_('Password'), widget=forms.PasswordInput,
max_length=128, strip=False, required=False, max_length=128, strip=False, required=False,
) )
role = forms.ChoiceField( role = forms.ChoiceField(
choices=role_choices, required=True, choices=role_choices, required=True,
initial=User.ROLE_USER, label=_("Role") initial=User.ROLE.USER, label=_("Role")
) )
source = forms.ChoiceField( source = forms.ChoiceField(
choices=get_source_choices, required=True, choices=get_source_choices, required=True,

View File

@ -18,8 +18,10 @@ from django.shortcuts import reverse
from common.local import LOCAL_DYNAMIC_SETTINGS from common.local import LOCAL_DYNAMIC_SETTINGS
from orgs.utils import current_org 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 import fields
from common.const import choices
from common.db.models import ChoiceSet
from ..signals import post_user_change_password from ..signals import post_user_change_password
@ -150,45 +152,58 @@ class AuthMixin:
class RoleMixin: class RoleMixin:
ROLE_ADMIN = 'Admin' class ROLE(ChoiceSet):
ROLE_USER = 'User' ADMIN = choices.ADMIN, _('Super administrator')
ROLE_APP = 'App' USER = choices.USER, _('User')
ROLE_AUDITOR = 'Auditor' AUDITOR = choices.AUDITOR, _('Super auditor')
APP = 'App', _('Application')
ROLE_CHOICES = ( role = ROLE.USER
(ROLE_ADMIN, _('Administrator')),
(ROLE_USER, _('User')),
(ROLE_APP, _('Application')),
(ROLE_AUDITOR, _("Auditor"))
)
role = ROLE_USER
@property @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(): if not current_org.is_real():
return self.get_role_display() if self.is_superuser:
roles = [] return ORG_ROLE.ADMIN.label
if self in current_org.get_org_admins(): else:
roles.append(str(_('Org admin'))) return ORG_ROLE.USER.label
if self in current_org.get_org_auditors():
roles.append(str(_('Org auditor'))) if hasattr(self, 'gc_m2m_org_members__role'):
if self in current_org.get_org_users(): names = self.gc_m2m_org_members__role
roles.append(str(_('User'))) if isinstance(names, str):
return " | ".join(roles) 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): def current_org_roles(self):
roles = [] from orgs.models import OrganizationMember, ROLE as ORG_ROLE
if self.can_admin_current_org: if not current_org.is_real():
roles.append('Admin') if self.is_superuser:
if self.can_audit_current_org: return [ORG_ROLE.ADMIN]
roles.append('Auditor') else:
else: return [ORG_ROLE.USER]
roles.append('User')
roles = list(set(OrganizationMember.objects.filter(
org_id=current_org.id, user=self
).values_list('role', flat=True)))
return roles return roles
@property @property
def is_superuser(self): def is_superuser(self):
if self.role == 'Admin': if self.role == self.ROLE.ADMIN:
return True return True
else: else:
return False return False
@ -196,13 +211,13 @@ class RoleMixin:
@is_superuser.setter @is_superuser.setter
def is_superuser(self, value): def is_superuser(self, value):
if value is True: if value is True:
self.role = 'Admin' self.role = self.ROLE.ADMIN
else: else:
self.role = 'User' self.role = self.ROLE.USER
@property @property
def is_super_auditor(self): def is_super_auditor(self):
return self.role == 'Auditor' return self.role == self.ROLE.AUDITOR
@property @property
def is_common_user(self): def is_common_user(self):
@ -216,7 +231,7 @@ class RoleMixin:
@property @property
def is_app(self): def is_app(self):
return self.role == 'App' return self.role == self.ROLE.APP
@lazyproperty @lazyproperty
def user_orgs(self): def user_orgs(self):
@ -240,14 +255,16 @@ class RoleMixin:
@lazyproperty @lazyproperty
def is_org_admin(self): 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 return True
else: else:
return False return False
@lazyproperty @lazyproperty
def is_org_auditor(self): 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 return True
else: else:
return False return False
@ -283,7 +300,7 @@ class RoleMixin:
def create_app_user(cls, name, comment): def create_app_user(cls, name, comment):
app = cls.objects.create( app = cls.objects.create(
username=name, name=name, email='{}@local.domain'.format(name), 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' is_first_login=False, created_by='System'
) )
access_key = app.create_access_key() access_key = app.create_access_key()
@ -473,7 +490,7 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
blank=True, verbose_name=_('User group') blank=True, verbose_name=_('User group')
) )
role = models.CharField( 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') blank=True, verbose_name=_('Role')
) )
avatar = models.ImageField( avatar = models.ImageField(
@ -526,6 +543,12 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
@property @property
def groups_display(self): 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()]) return ' '.join([group.name for group in self.groups.all()])
@property @property
@ -646,7 +669,7 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
email=forgery_py.internet.email_address(), email=forgery_py.internet.email_address(),
name=forgery_py.name.full_name(), name=forgery_py.name.full_name(),
password=make_password(forgery_py.lorem_ipsum.word()), 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), wechat=forgery_py.internet.user_name(True),
comment=forgery_py.lorem_ipsum.sentence(), comment=forgery_py.lorem_ipsum.sentence(),
created_by=choice(cls.objects.all()).username) created_by=choice(cls.objects.all()).username)

View File

@ -9,7 +9,8 @@ from common.utils import validate_ssh_public_key
from common.mixins import CommonBulkSerializerMixin from common.mixins import CommonBulkSerializerMixin
from common.serializers import AdaptedBulkListSerializer from common.serializers import AdaptedBulkListSerializer
from common.permissions import CanUpdateDeleteUser from common.permissions import CanUpdateDeleteUser
from ..models import User from common.drf.fields import GroupConcatedPrimaryKeyRelatedField
from ..models import User, UserGroup
__all__ = [ __all__ = [
@ -38,10 +39,16 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer):
label=_('Password strategy'), write_only=True label=_('Password strategy'), write_only=True
) )
mfa_level_display = serializers.ReadOnlyField(source='get_mfa_level_display') 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() login_blocked = serializers.SerializerMethodField()
can_update = serializers.SerializerMethodField() can_update = serializers.SerializerMethodField()
can_delete = 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_{}" key_prefix_block = "_LOGIN_BLOCK_{}"
class Meta: class Meta:
@ -52,7 +59,7 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer):
# small 指的是 不需要计算的直接能从一张表中获取到的数据 # small 指的是 不需要计算的直接能从一张表中获取到的数据
fields_small = fields_mini + [ fields_small = fields_mini + [
'password', 'email', 'public_key', 'wechat', 'phone', 'mfa_level', 'mfa_enabled', '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', 'comment', 'source', 'is_valid', 'is_expired',
'is_active', 'created_by', 'is_first_login', 'is_active', 'created_by', 'is_first_login',
'password_strategy', 'date_password_last_updated', 'date_expired', 'password_strategy', 'date_password_last_updated', 'date_expired',
@ -60,7 +67,7 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer):
] ]
fields = fields_small + [ fields = fields_small + [
'groups', 'role', 'groups_display', 'role_display', 'groups', 'role', 'groups_display', 'role_display',
'can_update', 'can_delete', 'login_blocked', 'can_update', 'can_delete', 'login_blocked', 'org_role'
] ]
extra_kwargs = { extra_kwargs = {
@ -75,7 +82,8 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer):
'can_delete': {'read_only': True}, 'can_delete': {'read_only': True},
'groups_display': {'label': _('Groups name')}, 'groups_display': {'label': _('Groups name')},
'source_display': {'label': _('Source 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): def __init__(self, *args, **kwargs):
@ -87,17 +95,17 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer):
if not role: if not role:
return return
choices = role._choices choices = role._choices
choices.pop(User.ROLE_APP, None) choices.pop(User.ROLE.APP, None)
request = self.context.get('request') request = self.context.get('request')
if request and hasattr(request, 'user') and not request.user.is_superuser: if request and hasattr(request, 'user') and not request.user.is_superuser:
choices.pop(User.ROLE_ADMIN, None) choices.pop(User.ROLE.ADMIN, None)
choices.pop(User.ROLE_AUDITOR, None) choices.pop(User.ROLE.AUDITOR, None)
role._choices = choices role._choices = choices
def validate_role(self, value): def validate_role(self, value):
request = self.context.get('request') request = self.context.get('request')
if not request.user.is_superuser and value != User.ROLE_USER: if not request.user.is_superuser and value != User.ROLE.USER:
role_display = dict(User.ROLE_CHOICES)[User.ROLE_USER] role_display = User.ROLE.USER.label
msg = _("Role limit to {}".format(role_display)) msg = _("Role limit to {}".format(role_display))
raise serializers.ValidationError(msg) raise serializers.ValidationError(msg)
return value return value
@ -121,7 +129,7 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer):
role = self.initial_data.get('role') role = self.initial_data.get('role')
if self.instance: if self.instance:
role = role or self.instance.role role = role or self.instance.role
if role == User.ROLE_AUDITOR: if role == User.ROLE.AUDITOR:
return [] return []
return groups return groups

View File

@ -71,7 +71,7 @@
{% endif %} {% endif %}
<tr> <tr>
<td>{% trans 'Role' %}:</td> <td>{% trans 'Role' %}:</td>
<td><b>{{ object.role_display }}</b></td> <td><b>{{ object.org_role_display }}</b></td>
</tr> </tr>
<tr> <tr>
<td>{% trans 'MFA' %}:</td> <td>{% trans 'MFA' %}:</td>

View File

@ -315,7 +315,7 @@ def construct_user_email(username, email):
def get_current_org_members(exclude=()): def get_current_org_members(exclude=()):
from orgs.utils import current_org 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(): def get_source_choices():