mirror of https://github.com/jumpserver/jumpserver
Merge pull request #10327 from jumpserver/pr@dev@json_m2m_field
pref: 自定义 ORM Field,使用 JSONField 完成pull/10539/head
commit
2262b0ecb5
|
@ -1,7 +1,7 @@
|
||||||
from common.api import JMSBulkModelViewSet
|
from common.api import JMSBulkModelViewSet
|
||||||
from ..models import LoginACL
|
|
||||||
from .. import serializers
|
from .. import serializers
|
||||||
from ..filters import LoginAclFilter
|
from ..filters import LoginAclFilter
|
||||||
|
from ..models import LoginACL
|
||||||
|
|
||||||
__all__ = ['LoginACLViewSet']
|
__all__ = ['LoginACLViewSet']
|
||||||
|
|
||||||
|
@ -11,4 +11,3 @@ class LoginACLViewSet(JMSBulkModelViewSet):
|
||||||
filterset_class = LoginAclFilter
|
filterset_class = LoginAclFilter
|
||||||
search_fields = ('name',)
|
search_fields = ('name',)
|
||||||
serializer_class = serializers.LoginACLSerializer
|
serializer_class = serializers.LoginACLSerializer
|
||||||
|
|
||||||
|
|
|
@ -30,14 +30,21 @@ class LoginAssetCheckAPI(CreateAPIView):
|
||||||
return serializer
|
return serializer
|
||||||
|
|
||||||
def check_review(self):
|
def check_review(self):
|
||||||
|
user = self.serializer.user
|
||||||
|
asset = self.serializer.asset
|
||||||
|
|
||||||
|
# 用户满足的 acls
|
||||||
|
queryset = LoginAssetACL.objects.all()
|
||||||
|
q = LoginAssetACL.users.get_filter_q(LoginAssetACL, 'users', user)
|
||||||
|
queryset = queryset.filter(q)
|
||||||
|
q = LoginAssetACL.assets.get_filter_q(LoginAssetACL, 'assets', asset)
|
||||||
|
queryset = queryset.filter(q)
|
||||||
|
account_username = self.serializer.validated_data.get('account_username')
|
||||||
|
queryset = queryset.filter(accounts__contains=account_username)
|
||||||
|
|
||||||
with tmp_to_org(self.serializer.asset.org):
|
with tmp_to_org(self.serializer.asset.org):
|
||||||
kwargs = {
|
acl = queryset.order_by('priority').valid().first()
|
||||||
'user': self.serializer.user,
|
|
||||||
'asset': self.serializer.asset,
|
|
||||||
'account_username': self.serializer.validated_data.get('account_username'),
|
|
||||||
'action': LoginAssetACL.ActionChoices.review
|
|
||||||
}
|
|
||||||
acl = LoginAssetACL.filter_queryset(**kwargs).valid().first()
|
|
||||||
if acl:
|
if acl:
|
||||||
need_review = True
|
need_review = True
|
||||||
response_data = self._get_response_data_of_need_review(acl)
|
response_data = self._get_response_data_of_need_review(acl)
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
# Generated by Django 3.2.17 on 2023-04-25 09:04
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
import common.db.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('acls', '0010_alter_commandfilteracl_command_groups'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='commandfilteracl',
|
||||||
|
name='new_accounts',
|
||||||
|
field=common.db.fields.JSONManyToManyField(default=dict, to='assets.Account', verbose_name='Accounts'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='commandfilteracl',
|
||||||
|
name='new_assets',
|
||||||
|
field=common.db.fields.JSONManyToManyField(default=dict, to='assets.Asset', verbose_name='Assets'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='commandfilteracl',
|
||||||
|
name='new_users',
|
||||||
|
field=common.db.fields.JSONManyToManyField(default=dict, to='users.User', verbose_name='Users'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='loginassetacl',
|
||||||
|
name='new_accounts',
|
||||||
|
field=common.db.fields.JSONManyToManyField(default=dict, to='assets.Account', verbose_name='Accounts'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='loginassetacl',
|
||||||
|
name='new_assets',
|
||||||
|
field=common.db.fields.JSONManyToManyField(default=dict, to='assets.Asset', verbose_name='Assets'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='loginassetacl',
|
||||||
|
name='new_users',
|
||||||
|
field=common.db.fields.JSONManyToManyField(default=dict, to='users.User', verbose_name='Users'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,42 @@
|
||||||
|
# Generated by Django 3.2.17 on 2023-04-26 03:11
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_base_acl_users_assets_accounts(apps, *args):
|
||||||
|
cmd_acl_model = apps.get_model('acls', 'CommandFilterACL')
|
||||||
|
login_asset_acl_model = apps.get_model('acls', 'LoginAssetACL')
|
||||||
|
|
||||||
|
for model in [cmd_acl_model, login_asset_acl_model]:
|
||||||
|
for obj in model.objects.all():
|
||||||
|
user_names = (obj.users or {}).get('username_group', [])
|
||||||
|
obj.new_users = {
|
||||||
|
"type": "attrs",
|
||||||
|
"attrs": [{"name": "username", "value": user_names, "match": "in"}]
|
||||||
|
}
|
||||||
|
|
||||||
|
asset_names = (obj.assets or {}).get('name_group', [])
|
||||||
|
asset_attrs = []
|
||||||
|
if asset_names:
|
||||||
|
asset_attrs.append({"name": "name", "value": asset_names, "match": "in"})
|
||||||
|
asset_address = (obj.assets or {}).get('address_group', [])
|
||||||
|
if asset_address:
|
||||||
|
asset_attrs.append({"name": "address", "value": asset_address, "match": "ip_in"})
|
||||||
|
obj.new_assets = {"type": "attrs", "attrs": asset_attrs}
|
||||||
|
|
||||||
|
account_usernames = (obj.accounts or {}).get('username_group', [])
|
||||||
|
obj.new_accounts = {
|
||||||
|
"type": "attrs",
|
||||||
|
"attrs": [{"name": "username", "value": account_usernames, "match": "in"}]
|
||||||
|
}
|
||||||
|
obj.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('acls', '0011_auto_20230425_1704'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(migrate_base_acl_users_assets_accounts)
|
||||||
|
]
|
|
@ -0,0 +1,66 @@
|
||||||
|
# Generated by Django 3.2.17 on 2023-04-26 09:59
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('acls', '0012_auto_20230426_1111'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='commandfilteracl',
|
||||||
|
name='accounts',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='commandfilteracl',
|
||||||
|
name='assets',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='commandfilteracl',
|
||||||
|
name='users',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='loginassetacl',
|
||||||
|
name='accounts',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='loginassetacl',
|
||||||
|
name='assets',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='loginassetacl',
|
||||||
|
name='users',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='commandfilteracl',
|
||||||
|
old_name='new_accounts',
|
||||||
|
new_name='accounts',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='commandfilteracl',
|
||||||
|
old_name='new_assets',
|
||||||
|
new_name='assets',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='commandfilteracl',
|
||||||
|
old_name='new_users',
|
||||||
|
new_name='users',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='loginassetacl',
|
||||||
|
old_name='new_accounts',
|
||||||
|
new_name='accounts',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='loginassetacl',
|
||||||
|
old_name='new_assets',
|
||||||
|
new_name='assets',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='loginassetacl',
|
||||||
|
old_name='new_users',
|
||||||
|
new_name='users',
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,18 +1,13 @@
|
||||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from common.db.fields import JSONManyToManyField
|
||||||
from common.db.models import JMSBaseModel
|
from common.db.models import JMSBaseModel
|
||||||
from common.utils import contains_ip
|
from orgs.mixins.models import OrgModelMixin
|
||||||
from orgs.mixins.models import OrgModelMixin, OrgManager
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'ACLManager',
|
'BaseACL', 'UserAssetAccountBaseACL',
|
||||||
'BaseACL',
|
|
||||||
'BaseACLQuerySet',
|
|
||||||
'UserAssetAccountBaseACL',
|
|
||||||
'UserAssetAccountACLQuerySet'
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -36,41 +31,6 @@ class BaseACLQuerySet(models.QuerySet):
|
||||||
return self.inactive()
|
return self.inactive()
|
||||||
|
|
||||||
|
|
||||||
class UserAssetAccountACLQuerySet(BaseACLQuerySet):
|
|
||||||
def filter_user(self, username):
|
|
||||||
q = Q(users__username_group__contains=username) | \
|
|
||||||
Q(users__username_group__contains='*')
|
|
||||||
return self.filter(q)
|
|
||||||
|
|
||||||
def filter_asset(self, name=None, address=None):
|
|
||||||
queryset = self.filter()
|
|
||||||
if name:
|
|
||||||
q = Q(assets__name_group__contains=name) | \
|
|
||||||
Q(assets__name_group__contains='*')
|
|
||||||
queryset = queryset.filter(q)
|
|
||||||
if address:
|
|
||||||
ids = [
|
|
||||||
q.id for q in queryset
|
|
||||||
if contains_ip(address, q.assets.get('address_group', []))
|
|
||||||
]
|
|
||||||
queryset = queryset.filter(id__in=ids)
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
def filter_account(self, username):
|
|
||||||
q = Q(accounts__username_group__contains=username) | \
|
|
||||||
Q(accounts__username_group__contains='*')
|
|
||||||
return self.filter(q)
|
|
||||||
|
|
||||||
|
|
||||||
class ACLManager(models.Manager):
|
|
||||||
def valid(self):
|
|
||||||
return self.get_queryset().valid()
|
|
||||||
|
|
||||||
|
|
||||||
class OrgACLManager(OrgManager, ACLManager):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class BaseACL(JMSBaseModel):
|
class BaseACL(JMSBaseModel):
|
||||||
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
||||||
priority = models.IntegerField(
|
priority = models.IntegerField(
|
||||||
|
@ -83,7 +43,7 @@ class BaseACL(JMSBaseModel):
|
||||||
is_active = models.BooleanField(default=True, verbose_name=_("Active"))
|
is_active = models.BooleanField(default=True, verbose_name=_("Active"))
|
||||||
|
|
||||||
ActionChoices = ActionChoices
|
ActionChoices = ActionChoices
|
||||||
objects = ACLManager.from_queryset(BaseACLQuerySet)()
|
objects = BaseACLQuerySet.as_manager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('priority', 'date_updated', 'name')
|
ordering = ('priority', 'date_updated', 'name')
|
||||||
|
@ -94,35 +54,10 @@ class BaseACL(JMSBaseModel):
|
||||||
|
|
||||||
|
|
||||||
class UserAssetAccountBaseACL(BaseACL, OrgModelMixin):
|
class UserAssetAccountBaseACL(BaseACL, OrgModelMixin):
|
||||||
# username_group
|
users = JSONManyToManyField('users.User', default=dict, verbose_name=_('Users'))
|
||||||
users = models.JSONField(verbose_name=_('User'))
|
assets = JSONManyToManyField('assets.Asset', default=dict, verbose_name=_('Assets'))
|
||||||
# name_group, address_group
|
accounts = models.JSONField(default=list, verbose_name=_("Account"))
|
||||||
assets = models.JSONField(verbose_name=_('Asset'))
|
|
||||||
# username_group
|
|
||||||
accounts = models.JSONField(verbose_name=_('Account'))
|
|
||||||
|
|
||||||
objects = OrgACLManager.from_queryset(UserAssetAccountACLQuerySet)()
|
|
||||||
|
|
||||||
class Meta(BaseACL.Meta):
|
class Meta(BaseACL.Meta):
|
||||||
unique_together = ('name', 'org_id')
|
unique_together = ('name', 'org_id')
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def filter_queryset(cls, user=None, asset=None, account=None, account_username=None, **kwargs):
|
|
||||||
queryset = cls.objects.all()
|
|
||||||
org_id = None
|
|
||||||
if user:
|
|
||||||
queryset = queryset.filter_user(user.username)
|
|
||||||
if account:
|
|
||||||
org_id = account.org_id
|
|
||||||
queryset = queryset.filter_account(account.username)
|
|
||||||
if account_username:
|
|
||||||
queryset = queryset.filter_account(username=account_username)
|
|
||||||
if asset:
|
|
||||||
org_id = asset.org_id
|
|
||||||
queryset = queryset.filter_asset(asset.name, asset.address)
|
|
||||||
if org_id:
|
|
||||||
kwargs['org_id'] = org_id
|
|
||||||
if kwargs:
|
|
||||||
queryset = queryset.filter(**kwargs)
|
|
||||||
return queryset
|
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
from .base import UserAssetAccountBaseACL
|
from .base import UserAssetAccountBaseACL
|
||||||
|
|
||||||
|
|
||||||
class LoginAssetACL(UserAssetAccountBaseACL):
|
class LoginAssetACL(UserAssetAccountBaseACL):
|
||||||
|
|
||||||
class Meta(UserAssetAccountBaseACL.Meta):
|
class Meta(UserAssetAccountBaseACL.Meta):
|
||||||
verbose_name = _('Login asset acl')
|
verbose_name = _('Login asset acl')
|
||||||
abstract = False
|
abstract = False
|
||||||
|
|
|
@ -2,8 +2,8 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from acls.models.base import ActionChoices
|
from acls.models.base import ActionChoices
|
||||||
from common.serializers.fields import LabeledChoiceField, ObjectRelatedField
|
|
||||||
from jumpserver.utils import has_valid_xpack_license
|
from jumpserver.utils import has_valid_xpack_license
|
||||||
|
from common.serializers.fields import JSONManyToManyField, ObjectRelatedField, LabeledChoiceField
|
||||||
from orgs.models import Organization
|
from orgs.models import Organization
|
||||||
from users.models import User
|
from users.models import User
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ class ACLUsersSerializer(serializers.Serializer):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ACLAssestsSerializer(serializers.Serializer):
|
class ACLAssetsSerializer(serializers.Serializer):
|
||||||
address_group_help_text = _(
|
address_group_help_text = _(
|
||||||
"With * indicating a match all. "
|
"With * indicating a match all. "
|
||||||
"Such as: "
|
"Such as: "
|
||||||
|
@ -72,25 +72,9 @@ class ActionAclSerializer(serializers.Serializer):
|
||||||
|
|
||||||
|
|
||||||
class BaseUserAssetAccountACLSerializerMixin(ActionAclSerializer, serializers.Serializer):
|
class BaseUserAssetAccountACLSerializerMixin(ActionAclSerializer, serializers.Serializer):
|
||||||
users = ACLUsersSerializer(label=_('User'))
|
users = JSONManyToManyField(label=_('User'))
|
||||||
assets = ACLAssestsSerializer(label=_('Asset'))
|
assets = JSONManyToManyField(label=_('Asset'))
|
||||||
accounts = ACLAccountsSerializer(label=_('Account'))
|
accounts = serializers.ListField(label=_('Account'))
|
||||||
users_username_group = serializers.ListField(
|
|
||||||
source='users.username_group', read_only=True, child=serializers.CharField(),
|
|
||||||
label=_('User (username)')
|
|
||||||
)
|
|
||||||
assets_name_group = serializers.ListField(
|
|
||||||
source='assets.name_group', read_only=True, child=serializers.CharField(),
|
|
||||||
label=_('Asset (name)')
|
|
||||||
)
|
|
||||||
assets_address_group = serializers.ListField(
|
|
||||||
source='assets.address_group', read_only=True, child=serializers.CharField(),
|
|
||||||
label=_('Asset (address)')
|
|
||||||
)
|
|
||||||
accounts_username_group = serializers.ListField(
|
|
||||||
source='accounts.username_group', read_only=True, child=serializers.CharField(),
|
|
||||||
label=_('Account (username)')
|
|
||||||
)
|
|
||||||
reviewers = ObjectRelatedField(
|
reviewers = ObjectRelatedField(
|
||||||
queryset=User.objects, many=True, required=False, label=_('Reviewers')
|
queryset=User.objects, many=True, required=False, label=_('Reviewers')
|
||||||
)
|
)
|
||||||
|
@ -101,8 +85,6 @@ class BaseUserAssetAccountACLSerializerMixin(ActionAclSerializer, serializers.Se
|
||||||
class Meta:
|
class Meta:
|
||||||
fields_mini = ["id", "name"]
|
fields_mini = ["id", "name"]
|
||||||
fields_small = fields_mini + [
|
fields_small = fields_mini + [
|
||||||
'users_username_group', 'assets_address_group', 'assets_name_group',
|
|
||||||
'accounts_username_group',
|
|
||||||
"users", "accounts", "assets", "is_active",
|
"users", "accounts", "assets", "is_active",
|
||||||
"date_created", "date_updated", "priority",
|
"date_created", "date_updated", "priority",
|
||||||
"action", "comment", "created_by", "org_id",
|
"action", "comment", "created_by", "org_id",
|
||||||
|
|
|
@ -15,7 +15,7 @@ from assets.filters import IpInFilterBackend, LabelFilterBackend, NodeFilterBack
|
||||||
from assets.models import Asset, Gateway, Platform
|
from assets.models import Asset, Gateway, Platform
|
||||||
from assets.tasks import test_assets_connectivity_manual, update_assets_hardware_info_manual
|
from assets.tasks import test_assets_connectivity_manual, update_assets_hardware_info_manual
|
||||||
from common.api import SuggestionMixin
|
from common.api import SuggestionMixin
|
||||||
from common.drf.filters import BaseFilterSet
|
from common.drf.filters import BaseFilterSet, AttrRulesFilterBackend
|
||||||
from common.utils import get_logger, is_uuid
|
from common.utils import get_logger, is_uuid
|
||||||
from orgs.mixins import generics
|
from orgs.mixins import generics
|
||||||
from orgs.mixins.api import OrgBulkModelViewSet
|
from orgs.mixins.api import OrgBulkModelViewSet
|
||||||
|
@ -110,7 +110,10 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet):
|
||||||
("spec_info", "assets.view_asset"),
|
("spec_info", "assets.view_asset"),
|
||||||
("gathered_info", "assets.view_asset"),
|
("gathered_info", "assets.view_asset"),
|
||||||
)
|
)
|
||||||
extra_filter_backends = [LabelFilterBackend, IpInFilterBackend, NodeFilterBackend]
|
extra_filter_backends = [
|
||||||
|
LabelFilterBackend, IpInFilterBackend,
|
||||||
|
NodeFilterBackend, AttrRulesFilterBackend
|
||||||
|
]
|
||||||
|
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
cls = super().get_serializer_class()
|
cls = super().get_serializer_class()
|
||||||
|
|
|
@ -6,6 +6,7 @@ import logging
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.db.models import Q
|
||||||
from django.forms import model_to_dict
|
from django.forms import model_to_dict
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
@ -116,7 +117,32 @@ class Protocol(models.Model):
|
||||||
return self.asset_platform_protocol.get('public', True)
|
return self.asset_platform_protocol.get('public', True)
|
||||||
|
|
||||||
|
|
||||||
class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel):
|
class JSONFilterMixin:
|
||||||
|
@staticmethod
|
||||||
|
def get_json_filter_attr_q(name, value, match):
|
||||||
|
"""
|
||||||
|
:param name: 属性名称
|
||||||
|
:param value: 定义的结果
|
||||||
|
:param match: 匹配方式
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
from ..node import Node
|
||||||
|
if not isinstance(value, (list, tuple)):
|
||||||
|
value = [value]
|
||||||
|
if name == 'nodes':
|
||||||
|
nodes = Node.objects.filter(id__in=value)
|
||||||
|
children = Node.get_nodes_all_children(nodes, with_self=True).values_list('id', flat=True)
|
||||||
|
return Q(nodes__in=children)
|
||||||
|
elif name == 'category':
|
||||||
|
return Q(platform__category__in=value)
|
||||||
|
elif name == 'type':
|
||||||
|
return Q(platform__type__in=value)
|
||||||
|
elif name == 'protocols':
|
||||||
|
return Q(protocols__name__in=value)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class Asset(NodesRelationMixin, AbsConnectivity, JSONFilterMixin, JMSOrgBaseModel):
|
||||||
Category = const.Category
|
Category = const.Category
|
||||||
Type = const.AllTypes
|
Type = const.AllTypes
|
||||||
|
|
||||||
|
|
|
@ -63,6 +63,19 @@ class FamilyMixin:
|
||||||
pattern += r'|^{0}$'.format(key)
|
pattern += r'|^{0}$'.format(key)
|
||||||
return pattern
|
return pattern
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_nodes_children_key_pattern(cls, nodes, with_self=True):
|
||||||
|
keys = [i.key for i in nodes]
|
||||||
|
keys = cls.clean_children_keys(keys)
|
||||||
|
patterns = [cls.get_node_all_children_key_pattern(key) for key in keys]
|
||||||
|
patterns = '|'.join(patterns)
|
||||||
|
return patterns
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_nodes_all_children(cls, nodes, with_self=True):
|
||||||
|
pattern = cls.get_nodes_children_key_pattern(nodes, with_self=with_self)
|
||||||
|
return Node.objects.filter(key__iregex=pattern)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_node_children_key_pattern(cls, key, with_self=True):
|
def get_node_children_key_pattern(cls, key, with_self=True):
|
||||||
pattern = r'^{0}:[0-9]+$'.format(key)
|
pattern = r'^{0}:[0-9]+$'.format(key)
|
||||||
|
|
|
@ -1,21 +1,20 @@
|
||||||
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from django.db import transaction
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
|
from django.db import transaction
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from common.utils import get_request_ip, get_logger
|
|
||||||
from common.utils.timezone import as_current_tz
|
|
||||||
from common.utils.encode import Singleton
|
|
||||||
from common.local import encrypted_field_set
|
from common.local import encrypted_field_set
|
||||||
from settings.serializers import SettingsSerializer
|
from common.utils import get_request_ip, get_logger
|
||||||
|
from common.utils.encode import Singleton
|
||||||
|
from common.utils.timezone import as_current_tz
|
||||||
from jumpserver.utils import current_request
|
from jumpserver.utils import current_request
|
||||||
from orgs.utils import get_current_org_id
|
|
||||||
from orgs.models import Organization
|
from orgs.models import Organization
|
||||||
|
from orgs.utils import get_current_org_id
|
||||||
|
from settings.serializers import SettingsSerializer
|
||||||
from .backends import get_operate_log_storage
|
from .backends import get_operate_log_storage
|
||||||
|
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -104,7 +103,9 @@ class OperatorLogHandler(metaclass=Singleton):
|
||||||
return ''
|
return ''
|
||||||
if isinstance(value[0], str):
|
if isinstance(value[0], str):
|
||||||
return ','.join(value)
|
return ','.join(value)
|
||||||
return ','.join([i['value'] for i in value if i.get('value')])
|
if isinstance(value[0], dict) and value[0].get('value') and isinstance(value[0]['value'], str):
|
||||||
|
return ','.join([str(i['value']) for i in value])
|
||||||
|
return json.dumps(value)
|
||||||
|
|
||||||
def __data_processing(self, dict_item, loop=True):
|
def __data_processing(self, dict_item, loop=True):
|
||||||
encrypt_value = '******'
|
encrypt_value = '******'
|
||||||
|
|
|
@ -1,15 +1,22 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
import json
|
|
||||||
|
|
||||||
|
import ipaddress
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.db.models import Q, Manager
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from rest_framework.utils.encoders import JSONEncoder
|
from rest_framework.utils.encoders import JSONEncoder
|
||||||
|
|
||||||
from common.local import add_encrypted_field_set
|
from common.local import add_encrypted_field_set
|
||||||
from common.utils import signer, crypto
|
from common.utils import signer, crypto, contains_ip
|
||||||
from .validators import PortRangeValidator
|
from .validators import PortRangeValidator
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
@ -32,6 +39,7 @@ __all__ = [
|
||||||
"PortRangeField",
|
"PortRangeField",
|
||||||
"BitChoices",
|
"BitChoices",
|
||||||
"TreeChoices",
|
"TreeChoices",
|
||||||
|
"JSONManyToManyField",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -274,3 +282,284 @@ class PortRangeField(models.CharField):
|
||||||
kwargs['max_length'] = 16
|
kwargs['max_length'] = 16
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.validators.append(PortRangeValidator())
|
self.validators.append(PortRangeValidator())
|
||||||
|
|
||||||
|
|
||||||
|
class RelatedManager:
|
||||||
|
def __init__(self, instance, field):
|
||||||
|
self.instance = instance
|
||||||
|
self.field = field
|
||||||
|
self.value = None
|
||||||
|
|
||||||
|
def set(self, value):
|
||||||
|
self.value = value
|
||||||
|
self.instance.__dict__[self.field.name] = value
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_filter_q(cls, value, to_model):
|
||||||
|
if not value or not isinstance(value, dict):
|
||||||
|
return Q()
|
||||||
|
|
||||||
|
if value["type"] == "all":
|
||||||
|
return Q()
|
||||||
|
elif value["type"] == "ids" and isinstance(value.get("ids"), list):
|
||||||
|
return Q(id__in=value["ids"])
|
||||||
|
elif value["type"] == "attrs" and isinstance(value.get("attrs"), list):
|
||||||
|
return cls._get_filter_attrs_q(value, to_model)
|
||||||
|
else:
|
||||||
|
return Q()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def filter_queryset_by_model(cls, value, to_model):
|
||||||
|
if hasattr(to_model, "get_queryset"):
|
||||||
|
queryset = to_model.get_queryset()
|
||||||
|
else:
|
||||||
|
queryset = to_model.objects.all()
|
||||||
|
q = cls.get_filter_q(value, to_model)
|
||||||
|
return queryset.filter(q).distinct()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_ip_in_q(name, val):
|
||||||
|
q = Q()
|
||||||
|
if isinstance(val, str):
|
||||||
|
val = [val]
|
||||||
|
for ip in val:
|
||||||
|
if not ip:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
if ip == '*':
|
||||||
|
return Q()
|
||||||
|
elif '/' in ip:
|
||||||
|
network = ipaddress.ip_network(ip)
|
||||||
|
ips = network.hosts()
|
||||||
|
q |= Q(**{"{}__in".format(name): ips})
|
||||||
|
elif '-' in ip:
|
||||||
|
start_ip, end_ip = ip.split('-')
|
||||||
|
start_ip = ipaddress.ip_address(start_ip)
|
||||||
|
end_ip = ipaddress.ip_address(end_ip)
|
||||||
|
q |= Q(**{"{}__range".format(name): (start_ip, end_ip)})
|
||||||
|
elif len(ip.split('.')) == 4:
|
||||||
|
q |= Q(**{"{}__exact".format(name): ip})
|
||||||
|
else:
|
||||||
|
q |= Q(**{"{}__startswith".format(name): ip})
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
return q
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_filter_attrs_q(cls, value, to_model):
|
||||||
|
filters = Q()
|
||||||
|
# 特殊情况有这几种,
|
||||||
|
# 1. 像 资产中的 type 和 category,集成自 Platform。所以不能直接查询
|
||||||
|
# 2. 像 资产中的 nodes,不是简单的 m2m,是树 的关系
|
||||||
|
# 3. 像 用户中的 orgs 也不是简单的 m2m,也是计算出来的
|
||||||
|
# get_filter_{}_attr_q 处理复杂的
|
||||||
|
custom_attr_filter = getattr(to_model, "get_json_filter_attr_q", None)
|
||||||
|
for attr in value["attrs"]:
|
||||||
|
if not isinstance(attr, dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
name = attr.get('name')
|
||||||
|
val = attr.get('value')
|
||||||
|
match = attr.get('match', 'exact')
|
||||||
|
if name is None or val is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if custom_attr_filter:
|
||||||
|
custom_filter_q = custom_attr_filter(name, val, match)
|
||||||
|
if custom_filter_q:
|
||||||
|
filters &= custom_filter_q
|
||||||
|
continue
|
||||||
|
|
||||||
|
if match == 'ip_in':
|
||||||
|
q = cls.get_ip_in_q(name, val)
|
||||||
|
elif match in ("exact", "contains", "startswith", "endswith", "regex", "gte", "lte", "gt", "lt"):
|
||||||
|
lookup = "{}__{}".format(name, match)
|
||||||
|
q = Q(**{lookup: val})
|
||||||
|
elif match == "not":
|
||||||
|
q = ~Q(**{name: val})
|
||||||
|
elif match in ['m2m', 'in']:
|
||||||
|
if not isinstance(val, list):
|
||||||
|
val = [val]
|
||||||
|
q = Q() if '*' in val else Q(**{"{}__in".format(name): val})
|
||||||
|
else:
|
||||||
|
q = Q() if val == '*' else Q(**{name: val})
|
||||||
|
filters &= q
|
||||||
|
return filters
|
||||||
|
|
||||||
|
def _get_queryset(self):
|
||||||
|
to_model = apps.get_model(self.field.to)
|
||||||
|
value = self.value
|
||||||
|
return self.filter_queryset_by_model(value, to_model)
|
||||||
|
|
||||||
|
def get_attr_q(self):
|
||||||
|
q = self._get_filter_attrs_q(self.value, apps.get_model(self.field.to))
|
||||||
|
return q
|
||||||
|
|
||||||
|
def all(self):
|
||||||
|
return self._get_queryset()
|
||||||
|
|
||||||
|
def filter(self, *args, **kwargs):
|
||||||
|
queryset = self._get_queryset()
|
||||||
|
return queryset.filter(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class JSONManyToManyDescriptor:
|
||||||
|
def __init__(self, field):
|
||||||
|
self.field = field
|
||||||
|
self._is_setting = False
|
||||||
|
|
||||||
|
def __get__(self, instance, owner=None):
|
||||||
|
if instance is None:
|
||||||
|
return self
|
||||||
|
|
||||||
|
if not hasattr(instance, "_related_manager_cache"):
|
||||||
|
instance._related_manager_cache = {}
|
||||||
|
if self.field.name not in instance._related_manager_cache:
|
||||||
|
manager = RelatedManager(instance, self.field)
|
||||||
|
instance._related_manager_cache[self.field.name] = manager
|
||||||
|
manager = instance._related_manager_cache[self.field.name]
|
||||||
|
return manager
|
||||||
|
|
||||||
|
def __set__(self, instance, value):
|
||||||
|
if instance is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not hasattr(instance, "_related_manager_cache"):
|
||||||
|
instance._related_manager_cache = {}
|
||||||
|
|
||||||
|
if self.field.name not in instance._related_manager_cache:
|
||||||
|
manager = self.__get__(instance, instance.__class__)
|
||||||
|
else:
|
||||||
|
manager = instance._related_manager_cache[self.field.name]
|
||||||
|
|
||||||
|
if isinstance(value, RelatedManager):
|
||||||
|
value = value.value
|
||||||
|
manager.set(value)
|
||||||
|
|
||||||
|
def is_match(self, obj, attr_rules):
|
||||||
|
# m2m 的情况
|
||||||
|
# 自定义的情况:比如 nodes, category
|
||||||
|
res = True
|
||||||
|
to_model = apps.get_model(self.field.to)
|
||||||
|
src_model = self.field.model
|
||||||
|
field_name = self.field.name
|
||||||
|
custom_attr_filter = getattr(src_model, "get_filter_{}_attr_q".format(field_name), None)
|
||||||
|
|
||||||
|
custom_q = Q()
|
||||||
|
for rule in attr_rules:
|
||||||
|
value = getattr(obj, rule['name'], '')
|
||||||
|
rule_value = rule.get('value', '')
|
||||||
|
rule_match = rule.get('match', 'exact')
|
||||||
|
|
||||||
|
if custom_attr_filter:
|
||||||
|
q = custom_attr_filter(rule['name'], rule_value, rule_match)
|
||||||
|
if q:
|
||||||
|
custom_q &= q
|
||||||
|
continue
|
||||||
|
|
||||||
|
if rule_match == 'in':
|
||||||
|
res &= value in rule_value
|
||||||
|
elif rule_match == 'exact':
|
||||||
|
res &= value == rule_value
|
||||||
|
elif rule_match == 'contains':
|
||||||
|
res &= rule_value in value
|
||||||
|
elif rule_match == 'startswith':
|
||||||
|
res &= str(value).startswith(str(rule_value))
|
||||||
|
elif rule_match == 'endswith':
|
||||||
|
res &= str(value).endswith(str(rule_value))
|
||||||
|
elif rule_match == 'regex':
|
||||||
|
res &= re.match(rule_value, value)
|
||||||
|
elif rule_match == 'not':
|
||||||
|
res &= value != rule_value
|
||||||
|
elif rule['match'] == 'gte':
|
||||||
|
res &= value >= rule_value
|
||||||
|
elif rule['match'] == 'lte':
|
||||||
|
res &= value <= rule_value
|
||||||
|
elif rule['match'] == 'gt':
|
||||||
|
res &= value > rule_value
|
||||||
|
elif rule['match'] == 'lt':
|
||||||
|
res &= value < rule_value
|
||||||
|
elif rule['match'] == 'ip_in':
|
||||||
|
if isinstance(rule_value, str):
|
||||||
|
rule_value = [rule_value]
|
||||||
|
res &= contains_ip(value, rule_value)
|
||||||
|
elif rule['match'] == 'm2m':
|
||||||
|
if isinstance(value, Manager):
|
||||||
|
value = value.values_list('id', flat=True)
|
||||||
|
value = set(map(str, value))
|
||||||
|
rule_value = set(map(str, rule_value))
|
||||||
|
res &= rule_value.issubset(value)
|
||||||
|
else:
|
||||||
|
logging.error("unknown match: {}".format(rule['match']))
|
||||||
|
res &= False
|
||||||
|
|
||||||
|
if not res:
|
||||||
|
return res
|
||||||
|
if custom_q:
|
||||||
|
res &= to_model.objects.filter(custom_q).filter(id=obj.id).exists()
|
||||||
|
return res
|
||||||
|
|
||||||
|
def get_filter_q(self, instance):
|
||||||
|
model_cls = self.field.model
|
||||||
|
field_name = self.field.column
|
||||||
|
q = Q(users__type='all') | Q(users__type='ids', users__ids__contains=[str(instance.id)])
|
||||||
|
queryset_id_attrs = model_cls.objects \
|
||||||
|
.filter(**{'{}__type'.format(field_name): 'attrs'}) \
|
||||||
|
.values_list('id', '{}__attrs'.format(field_name))
|
||||||
|
ids = [str(_id) for _id, attr_rules in queryset_id_attrs if self.is_match(instance, attr_rules)]
|
||||||
|
if ids:
|
||||||
|
q |= Q(id__in=ids)
|
||||||
|
return q
|
||||||
|
|
||||||
|
|
||||||
|
class JSONManyToManyField(models.JSONField):
|
||||||
|
def __init__(self, to, *args, **kwargs):
|
||||||
|
self.to = to
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def contribute_to_class(self, cls, name, **kwargs):
|
||||||
|
super().contribute_to_class(cls, name, **kwargs)
|
||||||
|
setattr(cls, self.name, JSONManyToManyDescriptor(self))
|
||||||
|
|
||||||
|
def deconstruct(self):
|
||||||
|
name, path, args, kwargs = super().deconstruct()
|
||||||
|
kwargs['to'] = self.to
|
||||||
|
return name, path, args, kwargs
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def check_value(val):
|
||||||
|
if not val:
|
||||||
|
return val
|
||||||
|
e = ValueError(_(
|
||||||
|
"Invalid JSON data for JSONManyToManyField, should be like "
|
||||||
|
"{'type': 'all'} or {'type': 'ids', 'ids': []} "
|
||||||
|
"or {'type': 'attrs', 'attrs': [{'name': 'ip', 'match': 'exact', 'value': '1.1.1.1'}}"
|
||||||
|
))
|
||||||
|
if not isinstance(val, dict):
|
||||||
|
raise e
|
||||||
|
if val["type"] not in ["all", "ids", "attrs"]:
|
||||||
|
raise ValueError(_('Invalid type, should be "all", "ids" or "attrs"'))
|
||||||
|
if val["type"] == "ids":
|
||||||
|
if not isinstance(val["ids"], list):
|
||||||
|
raise ValueError(_("Invalid ids for ids, should be a list"))
|
||||||
|
elif val["type"] == "attrs":
|
||||||
|
if not isinstance(val["attrs"], list):
|
||||||
|
raise ValueError(_("Invalid attrs, should be a list of dict"))
|
||||||
|
for attr in val["attrs"]:
|
||||||
|
if not isinstance(attr, dict):
|
||||||
|
raise ValueError(_("Invalid attrs, should be a list of dict"))
|
||||||
|
if 'name' not in attr or 'value' not in attr:
|
||||||
|
raise ValueError(_("Invalid attrs, should be has name and value"))
|
||||||
|
|
||||||
|
def get_prep_value(self, value):
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if isinstance(value, RelatedManager):
|
||||||
|
value = value.value
|
||||||
|
return json.dumps(value)
|
||||||
|
|
||||||
|
def validate(self, value, model_instance):
|
||||||
|
super().validate(value, model_instance)
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
raise ValidationError("Invalid JSON data for JSONManyToManyField.")
|
||||||
|
self.check_value(value)
|
||||||
|
|
|
@ -10,7 +10,6 @@
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
from functools import reduce
|
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
@ -55,7 +54,6 @@ def output_as_string(field_name):
|
||||||
|
|
||||||
|
|
||||||
class MultiTableChildQueryset(QuerySet):
|
class MultiTableChildQueryset(QuerySet):
|
||||||
|
|
||||||
def bulk_create(self, objs, batch_size=None):
|
def bulk_create(self, objs, batch_size=None):
|
||||||
assert batch_size is None or batch_size > 0
|
assert batch_size is None or batch_size > 0
|
||||||
if not objs:
|
if not objs:
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
|
@ -18,6 +20,8 @@ __all__ = [
|
||||||
"BaseFilterSet"
|
"BaseFilterSet"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
from common.db.fields import RelatedManager
|
||||||
|
|
||||||
|
|
||||||
class BaseFilterSet(drf_filters.FilterSet):
|
class BaseFilterSet(drf_filters.FilterSet):
|
||||||
def do_nothing(self, queryset, name, value):
|
def do_nothing(self, queryset, name, value):
|
||||||
|
@ -183,3 +187,32 @@ class UUIDInFilter(drf_filters.BaseInFilter, drf_filters.UUIDFilter):
|
||||||
|
|
||||||
class NumberInFilter(drf_filters.BaseInFilter, drf_filters.NumberFilter):
|
class NumberInFilter(drf_filters.BaseInFilter, drf_filters.NumberFilter):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AttrRulesFilterBackend(filters.BaseFilterBackend):
|
||||||
|
def get_schema_fields(self, view):
|
||||||
|
return [
|
||||||
|
coreapi.Field(
|
||||||
|
name='attr_rules', location='query', required=False,
|
||||||
|
type='string', example='/api/v1/users/users?attr_rules=jsonbase64',
|
||||||
|
description='Filter by json like {"type": "attrs", "attrs": []} to base64'
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
def filter_queryset(self, request, queryset, view):
|
||||||
|
attr_rules = request.query_params.get('attr_rules')
|
||||||
|
if not attr_rules:
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
try:
|
||||||
|
attr_rules = base64.b64decode(attr_rules.encode('utf-8'))
|
||||||
|
except Exception:
|
||||||
|
raise ValidationError({'attr_rules': 'attr_rules should be base64'})
|
||||||
|
try:
|
||||||
|
attr_rules = json.loads(attr_rules)
|
||||||
|
except Exception:
|
||||||
|
raise ValidationError({'attr_rules': 'attr_rules should be json'})
|
||||||
|
|
||||||
|
logging.debug('attr_rules: %s', attr_rules)
|
||||||
|
q = RelatedManager.get_filter_q(attr_rules, queryset.model)
|
||||||
|
return queryset.filter(q).distinct()
|
||||||
|
|
|
@ -7,7 +7,7 @@ from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.fields import ChoiceField, empty
|
from rest_framework.fields import ChoiceField, empty
|
||||||
|
|
||||||
from common.db.fields import TreeChoices
|
from common.db.fields import TreeChoices, JSONManyToManyField as ModelJSONManyToManyField
|
||||||
from common.local import add_encrypted_field_set
|
from common.local import add_encrypted_field_set
|
||||||
from common.utils import decrypt_password
|
from common.utils import decrypt_password
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ __all__ = [
|
||||||
"TreeChoicesField",
|
"TreeChoicesField",
|
||||||
"LabeledMultipleChoiceField",
|
"LabeledMultipleChoiceField",
|
||||||
"PhoneField",
|
"PhoneField",
|
||||||
|
"JSONManyToManyField"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -216,3 +217,26 @@ class PhoneField(serializers.CharField):
|
||||||
phone = phonenumbers.parse(value, 'CN')
|
phone = phonenumbers.parse(value, 'CN')
|
||||||
value = {'code': '+%s' % phone.country_code, 'phone': phone.national_number}
|
value = {'code': '+%s' % phone.country_code, 'phone': phone.national_number}
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class JSONManyToManyField(serializers.JSONField):
|
||||||
|
def to_representation(self, manager):
|
||||||
|
if manager is None:
|
||||||
|
return manager
|
||||||
|
value = manager.value
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
return {"type": "ids", "ids": []}
|
||||||
|
if value.get("type") == "ids":
|
||||||
|
valid_ids = manager.all().values_list("id", flat=True)
|
||||||
|
valid_ids = [str(i) for i in valid_ids]
|
||||||
|
return {"type": "ids", "ids": valid_ids}
|
||||||
|
return value
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
if not data:
|
||||||
|
data = {}
|
||||||
|
try:
|
||||||
|
ModelJSONManyToManyField.check_value(data)
|
||||||
|
except ValueError as e:
|
||||||
|
raise serializers.ValidationError(e)
|
||||||
|
return super().to_internal_value(data)
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import ipaddress
|
||||||
import socket
|
import socket
|
||||||
from ipaddress import ip_network, ip_address
|
from ipaddress import ip_network, ip_address
|
||||||
|
|
||||||
|
@ -75,6 +76,23 @@ def contains_ip(ip, ip_group):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def is_ip(self, ip, rule_value):
|
||||||
|
if rule_value == '*':
|
||||||
|
return True
|
||||||
|
elif '/' in rule_value:
|
||||||
|
network = ipaddress.ip_network(rule_value)
|
||||||
|
return ip in network.hosts()
|
||||||
|
elif '-' in rule_value:
|
||||||
|
start_ip, end_ip = rule_value.split('-')
|
||||||
|
start_ip = ipaddress.ip_address(start_ip)
|
||||||
|
end_ip = ipaddress.ip_address(end_ip)
|
||||||
|
return start_ip <= ip <= end_ip
|
||||||
|
elif len(rule_value.split('.')) == 4:
|
||||||
|
return ip == rule_value
|
||||||
|
else:
|
||||||
|
return ip.startswith(rule_value)
|
||||||
|
|
||||||
|
|
||||||
def get_ip_city(ip):
|
def get_ip_city(ip):
|
||||||
if not ip or not isinstance(ip, str):
|
if not ip or not isinstance(ip, str):
|
||||||
return _("Invalid address")
|
return _("Invalid address")
|
||||||
|
|
|
@ -2,8 +2,8 @@ from django.db import models
|
||||||
from django.db.models import F, TextChoices
|
from django.db.models import F, TextChoices
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from assets.models import Asset, Node, FamilyMixin
|
|
||||||
from accounts.models import Account
|
from accounts.models import Account
|
||||||
|
from assets.models import Asset, Node, FamilyMixin
|
||||||
from common.utils import lazyproperty
|
from common.utils import lazyproperty
|
||||||
from orgs.mixins.models import JMSOrgBaseModel
|
from orgs.mixins.models import JMSOrgBaseModel
|
||||||
|
|
||||||
|
|
|
@ -44,25 +44,12 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer):
|
||||||
model = AssetPermission
|
model = AssetPermission
|
||||||
fields_mini = ["id", "name"]
|
fields_mini = ["id", "name"]
|
||||||
fields_generic = [
|
fields_generic = [
|
||||||
"accounts",
|
"accounts", "actions", "created_by", "date_created",
|
||||||
"actions",
|
"date_start", "date_expired", "is_active", "is_expired",
|
||||||
"created_by",
|
"is_valid", "comment", "from_ticket",
|
||||||
"date_created",
|
|
||||||
"date_start",
|
|
||||||
"date_expired",
|
|
||||||
"is_active",
|
|
||||||
"is_expired",
|
|
||||||
"is_valid",
|
|
||||||
"comment",
|
|
||||||
"from_ticket",
|
|
||||||
]
|
]
|
||||||
fields_small = fields_mini + fields_generic
|
fields_small = fields_mini + fields_generic
|
||||||
fields_m2m = [
|
fields_m2m = ["users", "user_groups", "assets", "nodes"]
|
||||||
"users",
|
|
||||||
"user_groups",
|
|
||||||
"assets",
|
|
||||||
"nodes",
|
|
||||||
]
|
|
||||||
fields = fields_mini + fields_m2m + fields_generic
|
fields = fields_mini + fields_m2m + fields_generic
|
||||||
read_only_fields = ["created_by", "date_created", "from_ticket"]
|
read_only_fields = ["created_by", "date_created", "from_ticket"]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
|
@ -91,7 +78,8 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer):
|
||||||
def create_accounts(self, assets):
|
def create_accounts(self, assets):
|
||||||
need_create_accounts = []
|
need_create_accounts = []
|
||||||
account_attribute = [
|
account_attribute = [
|
||||||
'name', 'username', 'secret_type', 'secret', 'privileged', 'is_active', 'org_id'
|
'name', 'username', 'secret_type', 'secret',
|
||||||
|
'privileged', 'is_active', 'org_id'
|
||||||
]
|
]
|
||||||
for asset in assets:
|
for asset in assets:
|
||||||
asset_exist_accounts = Account.objects.none()
|
asset_exist_accounts = Account.objects.none()
|
||||||
|
@ -140,10 +128,7 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer):
|
||||||
def setup_eager_loading(cls, queryset):
|
def setup_eager_loading(cls, queryset):
|
||||||
"""Perform necessary eager loading of data."""
|
"""Perform necessary eager loading of data."""
|
||||||
queryset = queryset.prefetch_related(
|
queryset = queryset.prefetch_related(
|
||||||
"users",
|
"users", "user_groups", "assets", "nodes",
|
||||||
"user_groups",
|
|
||||||
"assets",
|
|
||||||
"nodes",
|
|
||||||
)
|
)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
|
@ -101,6 +101,7 @@ class StepAction:
|
||||||
else:
|
else:
|
||||||
driver.switch_to.frame(target)
|
driver.switch_to.frame(target)
|
||||||
|
|
||||||
|
|
||||||
def execute_action(driver: webdriver.Chrome, step: StepAction) -> bool:
|
def execute_action(driver: webdriver.Chrome, step: StepAction) -> bool:
|
||||||
try:
|
try:
|
||||||
return step.execute(driver)
|
return step.execute(driver)
|
||||||
|
@ -197,8 +198,10 @@ def default_chrome_driver_options():
|
||||||
# 禁用开发者工具
|
# 禁用开发者工具
|
||||||
options.add_argument("--disable-dev-tools")
|
options.add_argument("--disable-dev-tools")
|
||||||
# 禁用 密码管理器弹窗
|
# 禁用 密码管理器弹窗
|
||||||
prefs = {"credentials_enable_service": False,
|
prefs = {
|
||||||
"profile.password_manager_enabled": False}
|
"credentials_enable_service": False,
|
||||||
|
"profile.password_manager_enabled": False
|
||||||
|
}
|
||||||
options.add_experimental_option("prefs", prefs)
|
options.add_experimental_option("prefs", prefs)
|
||||||
options.add_experimental_option("excludeSwitches", ['enable-automation'])
|
options.add_experimental_option("excludeSwitches", ['enable-automation'])
|
||||||
return options
|
return options
|
||||||
|
|
|
@ -7,8 +7,8 @@ from rest_framework.decorators import action
|
||||||
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.api import CommonApiMixin
|
from common.api import CommonApiMixin, SuggestionMixin
|
||||||
from common.api import SuggestionMixin
|
from common.drf.filters import AttrRulesFilterBackend
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from orgs.utils import current_org, tmp_to_root_org
|
from orgs.utils import current_org, tmp_to_root_org
|
||||||
from rbac.models import Role, RoleBinding
|
from rbac.models import Role, RoleBinding
|
||||||
|
@ -35,6 +35,7 @@ __all__ = [
|
||||||
|
|
||||||
class UserViewSet(CommonApiMixin, UserQuerysetMixin, SuggestionMixin, BulkModelViewSet):
|
class UserViewSet(CommonApiMixin, UserQuerysetMixin, SuggestionMixin, BulkModelViewSet):
|
||||||
filterset_class = UserFilter
|
filterset_class = UserFilter
|
||||||
|
extra_filter_backends = [AttrRulesFilterBackend]
|
||||||
search_fields = ('username', 'email', 'name')
|
search_fields = ('username', 'email', 'name')
|
||||||
permission_classes = [RBACPermission, UserObjectPermission]
|
permission_classes = [RBACPermission, UserObjectPermission]
|
||||||
serializer_classes = {
|
serializer_classes = {
|
||||||
|
|
|
@ -668,7 +668,33 @@ class MFAMixin:
|
||||||
return backend
|
return backend
|
||||||
|
|
||||||
|
|
||||||
class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
|
class JSONFilterMixin:
|
||||||
|
"""
|
||||||
|
users = JSONManyToManyField('users.User', blank=True, null=True)
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_json_filter_attr_q(name, value, match):
|
||||||
|
from rbac.models import RoleBinding
|
||||||
|
from orgs.utils import current_org
|
||||||
|
|
||||||
|
if name == 'system_roles':
|
||||||
|
user_id = RoleBinding.objects \
|
||||||
|
.filter(role__in=value, scope='system') \
|
||||||
|
.values_list('user_id', flat=True)
|
||||||
|
return models.Q(id__in=user_id)
|
||||||
|
elif name == 'org_roles':
|
||||||
|
kwargs = dict(role__in=value, scope='org')
|
||||||
|
if not current_org.is_root():
|
||||||
|
kwargs['org_id'] = current_org.id
|
||||||
|
|
||||||
|
user_id = RoleBinding.objects.filter(**kwargs) \
|
||||||
|
.values_list('user_id', flat=True)
|
||||||
|
return models.Q(id__in=user_id)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, JSONFilterMixin, AbstractUser):
|
||||||
class Source(models.TextChoices):
|
class Source(models.TextChoices):
|
||||||
local = 'local', _('Local')
|
local = 'local', _('Local')
|
||||||
ldap = 'ldap', 'LDAP/AD'
|
ldap = 'ldap', 'LDAP/AD'
|
||||||
|
|
Loading…
Reference in New Issue