Merge pull request #10327 from jumpserver/pr@dev@json_m2m_field

pref: 自定义 ORM Field,使用 JSONField 完成
pull/10539/head
老广 2023-05-24 15:27:47 +08:00 committed by GitHub
commit 2262b0ecb5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 644 additions and 151 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = '******'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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