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

View File

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

View File

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

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):
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
)

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
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():

Binary file not shown.

View File

@ -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 <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/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 ""
" </div>\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 "组织详情"

View File

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

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

View File

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

View File

@ -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):
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()
def _clear_users_from_org(org, users):
if not users:
return
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(org)
_remove_users(AssetPermission, users, org)
_remove_users(UserGroup, users, org, reverse=True)
set_current_org(old_org)
@receiver(m2m_changed, sender=Organization.admins.through)
def on_org_admin_change(sender, **kwargs):
Organization._user_admin_orgs = None
@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'
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')
old_version_urlpatterns = [

View File

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

View File

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

View File

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

View File

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

View File

@ -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):
if not current_org.is_real():
def super_role_display(self):
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)
@property
def org_role_display(self):
from orgs.models import ROLE as ORG_ROLE
if not current_org.is_real():
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')
from orgs.models import OrganizationMember, ROLE as ORG_ROLE
if not current_org.is_real():
if self.is_superuser:
return [ORG_ROLE.ADMIN]
else:
roles.append('User')
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)

View File

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

View File

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

View File

@ -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():