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 ..models import LoginACL
|
||||
from .. import serializers
|
||||
from ..filters import LoginAclFilter
|
||||
from ..models import LoginACL
|
||||
|
||||
__all__ = ['LoginACLViewSet']
|
||||
|
||||
|
@ -11,4 +11,3 @@ class LoginACLViewSet(JMSBulkModelViewSet):
|
|||
filterset_class = LoginAclFilter
|
||||
search_fields = ('name',)
|
||||
serializer_class = serializers.LoginACLSerializer
|
||||
|
||||
|
|
|
@ -30,14 +30,21 @@ class LoginAssetCheckAPI(CreateAPIView):
|
|||
return serializer
|
||||
|
||||
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):
|
||||
kwargs = {
|
||||
'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()
|
||||
acl = queryset.order_by('priority').valid().first()
|
||||
|
||||
if acl:
|
||||
need_review = True
|
||||
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.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.db.fields import JSONManyToManyField
|
||||
from common.db.models import JMSBaseModel
|
||||
from common.utils import contains_ip
|
||||
from orgs.mixins.models import OrgModelMixin, OrgManager
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
|
||||
__all__ = [
|
||||
'ACLManager',
|
||||
'BaseACL',
|
||||
'BaseACLQuerySet',
|
||||
'UserAssetAccountBaseACL',
|
||||
'UserAssetAccountACLQuerySet'
|
||||
'BaseACL', 'UserAssetAccountBaseACL',
|
||||
]
|
||||
|
||||
|
||||
|
@ -36,41 +31,6 @@ class BaseACLQuerySet(models.QuerySet):
|
|||
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):
|
||||
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
||||
priority = models.IntegerField(
|
||||
|
@ -83,7 +43,7 @@ class BaseACL(JMSBaseModel):
|
|||
is_active = models.BooleanField(default=True, verbose_name=_("Active"))
|
||||
|
||||
ActionChoices = ActionChoices
|
||||
objects = ACLManager.from_queryset(BaseACLQuerySet)()
|
||||
objects = BaseACLQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ('priority', 'date_updated', 'name')
|
||||
|
@ -94,35 +54,10 @@ class BaseACL(JMSBaseModel):
|
|||
|
||||
|
||||
class UserAssetAccountBaseACL(BaseACL, OrgModelMixin):
|
||||
# username_group
|
||||
users = models.JSONField(verbose_name=_('User'))
|
||||
# name_group, address_group
|
||||
assets = models.JSONField(verbose_name=_('Asset'))
|
||||
# username_group
|
||||
accounts = models.JSONField(verbose_name=_('Account'))
|
||||
|
||||
objects = OrgACLManager.from_queryset(UserAssetAccountACLQuerySet)()
|
||||
users = JSONManyToManyField('users.User', default=dict, verbose_name=_('Users'))
|
||||
assets = JSONManyToManyField('assets.Asset', default=dict, verbose_name=_('Assets'))
|
||||
accounts = models.JSONField(default=list, verbose_name=_("Account"))
|
||||
|
||||
class Meta(BaseACL.Meta):
|
||||
unique_together = ('name', 'org_id')
|
||||
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 .base import UserAssetAccountBaseACL
|
||||
|
||||
|
||||
class LoginAssetACL(UserAssetAccountBaseACL):
|
||||
|
||||
class Meta(UserAssetAccountBaseACL.Meta):
|
||||
verbose_name = _('Login asset acl')
|
||||
abstract = False
|
||||
|
|
|
@ -2,8 +2,8 @@ from django.utils.translation import ugettext_lazy as _
|
|||
from rest_framework import serializers
|
||||
|
||||
from acls.models.base import ActionChoices
|
||||
from common.serializers.fields import LabeledChoiceField, ObjectRelatedField
|
||||
from jumpserver.utils import has_valid_xpack_license
|
||||
from common.serializers.fields import JSONManyToManyField, ObjectRelatedField, LabeledChoiceField
|
||||
from orgs.models import Organization
|
||||
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 = _(
|
||||
"With * indicating a match all. "
|
||||
"Such as: "
|
||||
|
@ -72,25 +72,9 @@ class ActionAclSerializer(serializers.Serializer):
|
|||
|
||||
|
||||
class BaseUserAssetAccountACLSerializerMixin(ActionAclSerializer, serializers.Serializer):
|
||||
users = ACLUsersSerializer(label=_('User'))
|
||||
assets = ACLAssestsSerializer(label=_('Asset'))
|
||||
accounts = ACLAccountsSerializer(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)')
|
||||
)
|
||||
users = JSONManyToManyField(label=_('User'))
|
||||
assets = JSONManyToManyField(label=_('Asset'))
|
||||
accounts = serializers.ListField(label=_('Account'))
|
||||
reviewers = ObjectRelatedField(
|
||||
queryset=User.objects, many=True, required=False, label=_('Reviewers')
|
||||
)
|
||||
|
@ -101,8 +85,6 @@ class BaseUserAssetAccountACLSerializerMixin(ActionAclSerializer, serializers.Se
|
|||
class Meta:
|
||||
fields_mini = ["id", "name"]
|
||||
fields_small = fields_mini + [
|
||||
'users_username_group', 'assets_address_group', 'assets_name_group',
|
||||
'accounts_username_group',
|
||||
"users", "accounts", "assets", "is_active",
|
||||
"date_created", "date_updated", "priority",
|
||||
"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.tasks import test_assets_connectivity_manual, update_assets_hardware_info_manual
|
||||
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 orgs.mixins import generics
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
|
@ -110,7 +110,10 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet):
|
|||
("spec_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):
|
||||
cls = super().get_serializer_class()
|
||||
|
|
|
@ -6,6 +6,7 @@ import logging
|
|||
from collections import defaultdict
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.forms import model_to_dict
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
@ -116,7 +117,32 @@ class Protocol(models.Model):
|
|||
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
|
||||
Type = const.AllTypes
|
||||
|
||||
|
|
|
@ -63,6 +63,19 @@ class FamilyMixin:
|
|||
pattern += r'|^{0}$'.format(key)
|
||||
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
|
||||
def get_node_children_key_pattern(cls, key, with_self=True):
|
||||
pattern = r'^{0}:[0-9]+$'.format(key)
|
||||
|
|
|
@ -1,21 +1,20 @@
|
|||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from django.db import transaction
|
||||
from django.core.cache import cache
|
||||
from django.db import transaction
|
||||
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 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 orgs.utils import get_current_org_id
|
||||
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
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
|
@ -104,7 +103,9 @@ class OperatorLogHandler(metaclass=Singleton):
|
|||
return ''
|
||||
if isinstance(value[0], str):
|
||||
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):
|
||||
encrypt_value = '******'
|
||||
|
|
|
@ -1,15 +1,22 @@
|
|||
# -*- 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.db import models
|
||||
from django.db.models import Q, Manager
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework.utils.encoders import JSONEncoder
|
||||
|
||||
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
|
||||
|
||||
__all__ = [
|
||||
|
@ -32,6 +39,7 @@ __all__ = [
|
|||
"PortRangeField",
|
||||
"BitChoices",
|
||||
"TreeChoices",
|
||||
"JSONManyToManyField",
|
||||
]
|
||||
|
||||
|
||||
|
@ -274,3 +282,284 @@ class PortRangeField(models.CharField):
|
|||
kwargs['max_length'] = 16
|
||||
super().__init__(**kwargs)
|
||||
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
|
||||
from functools import reduce
|
||||
|
||||
from django.db import models
|
||||
from django.db import transaction
|
||||
|
@ -55,7 +54,6 @@ def output_as_string(field_name):
|
|||
|
||||
|
||||
class MultiTableChildQueryset(QuerySet):
|
||||
|
||||
def bulk_create(self, objs, batch_size=None):
|
||||
assert batch_size is None or batch_size > 0
|
||||
if not objs:
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
|
||||
from django.core.cache import cache
|
||||
|
@ -18,6 +20,8 @@ __all__ = [
|
|||
"BaseFilterSet"
|
||||
]
|
||||
|
||||
from common.db.fields import RelatedManager
|
||||
|
||||
|
||||
class BaseFilterSet(drf_filters.FilterSet):
|
||||
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):
|
||||
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.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.utils import decrypt_password
|
||||
|
||||
|
@ -20,6 +20,7 @@ __all__ = [
|
|||
"TreeChoicesField",
|
||||
"LabeledMultipleChoiceField",
|
||||
"PhoneField",
|
||||
"JSONManyToManyField"
|
||||
]
|
||||
|
||||
|
||||
|
@ -216,3 +217,26 @@ class PhoneField(serializers.CharField):
|
|||
phone = phonenumbers.parse(value, 'CN')
|
||||
value = {'code': '+%s' % phone.country_code, 'phone': phone.national_number}
|
||||
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
|
||||
from ipaddress import ip_network, ip_address
|
||||
|
||||
|
@ -75,6 +76,23 @@ def contains_ip(ip, ip_group):
|
|||
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):
|
||||
if not ip or not isinstance(ip, str):
|
||||
return _("Invalid address")
|
||||
|
|
|
@ -2,8 +2,8 @@ from django.db import models
|
|||
from django.db.models import F, TextChoices
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from assets.models import Asset, Node, FamilyMixin
|
||||
from accounts.models import Account
|
||||
from assets.models import Asset, Node, FamilyMixin
|
||||
from common.utils import lazyproperty
|
||||
from orgs.mixins.models import JMSOrgBaseModel
|
||||
|
||||
|
|
|
@ -44,25 +44,12 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer):
|
|||
model = AssetPermission
|
||||
fields_mini = ["id", "name"]
|
||||
fields_generic = [
|
||||
"accounts",
|
||||
"actions",
|
||||
"created_by",
|
||||
"date_created",
|
||||
"date_start",
|
||||
"date_expired",
|
||||
"is_active",
|
||||
"is_expired",
|
||||
"is_valid",
|
||||
"comment",
|
||||
"from_ticket",
|
||||
"accounts", "actions", "created_by", "date_created",
|
||||
"date_start", "date_expired", "is_active", "is_expired",
|
||||
"is_valid", "comment", "from_ticket",
|
||||
]
|
||||
fields_small = fields_mini + fields_generic
|
||||
fields_m2m = [
|
||||
"users",
|
||||
"user_groups",
|
||||
"assets",
|
||||
"nodes",
|
||||
]
|
||||
fields_m2m = ["users", "user_groups", "assets", "nodes"]
|
||||
fields = fields_mini + fields_m2m + fields_generic
|
||||
read_only_fields = ["created_by", "date_created", "from_ticket"]
|
||||
extra_kwargs = {
|
||||
|
@ -91,7 +78,8 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer):
|
|||
def create_accounts(self, assets):
|
||||
need_create_accounts = []
|
||||
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:
|
||||
asset_exist_accounts = Account.objects.none()
|
||||
|
@ -140,10 +128,7 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer):
|
|||
def setup_eager_loading(cls, queryset):
|
||||
"""Perform necessary eager loading of data."""
|
||||
queryset = queryset.prefetch_related(
|
||||
"users",
|
||||
"user_groups",
|
||||
"assets",
|
||||
"nodes",
|
||||
"users", "user_groups", "assets", "nodes",
|
||||
)
|
||||
return queryset
|
||||
|
||||
|
|
|
@ -101,6 +101,7 @@ class StepAction:
|
|||
else:
|
||||
driver.switch_to.frame(target)
|
||||
|
||||
|
||||
def execute_action(driver: webdriver.Chrome, step: StepAction) -> bool:
|
||||
try:
|
||||
return step.execute(driver)
|
||||
|
@ -197,8 +198,10 @@ def default_chrome_driver_options():
|
|||
# 禁用开发者工具
|
||||
options.add_argument("--disable-dev-tools")
|
||||
# 禁用 密码管理器弹窗
|
||||
prefs = {"credentials_enable_service": False,
|
||||
"profile.password_manager_enabled": False}
|
||||
prefs = {
|
||||
"credentials_enable_service": False,
|
||||
"profile.password_manager_enabled": False
|
||||
}
|
||||
options.add_experimental_option("prefs", prefs)
|
||||
options.add_experimental_option("excludeSwitches", ['enable-automation'])
|
||||
return options
|
||||
|
|
|
@ -7,8 +7,8 @@ from rest_framework.decorators import action
|
|||
from rest_framework.response import Response
|
||||
from rest_framework_bulk import BulkModelViewSet
|
||||
|
||||
from common.api import CommonApiMixin
|
||||
from common.api import SuggestionMixin
|
||||
from common.api import CommonApiMixin, SuggestionMixin
|
||||
from common.drf.filters import AttrRulesFilterBackend
|
||||
from common.utils import get_logger
|
||||
from orgs.utils import current_org, tmp_to_root_org
|
||||
from rbac.models import Role, RoleBinding
|
||||
|
@ -35,6 +35,7 @@ __all__ = [
|
|||
|
||||
class UserViewSet(CommonApiMixin, UserQuerysetMixin, SuggestionMixin, BulkModelViewSet):
|
||||
filterset_class = UserFilter
|
||||
extra_filter_backends = [AttrRulesFilterBackend]
|
||||
search_fields = ('username', 'email', 'name')
|
||||
permission_classes = [RBACPermission, UserObjectPermission]
|
||||
serializer_classes = {
|
||||
|
|
|
@ -668,7 +668,33 @@ class MFAMixin:
|
|||
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):
|
||||
local = 'local', _('Local')
|
||||
ldap = 'ldap', 'LDAP/AD'
|
||||
|
|
Loading…
Reference in New Issue