diff --git a/apps/acls/migrations/0011_auto_20230425_1704.py b/apps/acls/migrations/0011_auto_20230425_1704.py new file mode 100644 index 000000000..2a8682129 --- /dev/null +++ b/apps/acls/migrations/0011_auto_20230425_1704.py @@ -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'), + ), + ] diff --git a/apps/acls/migrations/0012_auto_20230426_1111.py b/apps/acls/migrations/0012_auto_20230426_1111.py new file mode 100644 index 000000000..23984ac30 --- /dev/null +++ b/apps/acls/migrations/0012_auto_20230426_1111.py @@ -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", "rel": "or"}) + 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) + ] diff --git a/apps/acls/migrations/0013_auto_20230426_1759.py b/apps/acls/migrations/0013_auto_20230426_1759.py new file mode 100644 index 000000000..56dd58446 --- /dev/null +++ b/apps/acls/migrations/0013_auto_20230426_1759.py @@ -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', + ), + ] diff --git a/apps/acls/models/base.py b/apps/acls/models/base.py index 2624e34cd..def536724 100644 --- a/apps/acls/models/base.py +++ b/apps/acls/models/base.py @@ -3,6 +3,7 @@ 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 @@ -95,11 +96,11 @@ class BaseACL(JMSBaseModel): class UserAssetAccountBaseACL(BaseACL, OrgModelMixin): # username_group - users = models.JSONField(verbose_name=_('User')) + users = JSONManyToManyField('users.User', default=dict, verbose_name=_('Users')) # name_group, address_group - assets = models.JSONField(verbose_name=_('Asset')) + assets = JSONManyToManyField('assets.Asset', default=dict, verbose_name=_('Assets')) # username_group - accounts = models.JSONField(verbose_name=_('Account')) + accounts = JSONManyToManyField('assets.Account', default=dict, verbose_name=_('Accounts')) objects = OrgACLManager.from_queryset(UserAssetAccountACLQuerySet)() diff --git a/apps/common/db/fields.py b/apps/common/db/fields.py index 7ff001e92..c80c2b84d 100644 --- a/apps/common/db/fields.py +++ b/apps/common/db/fields.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # +import ipaddress import json from django.apps import apps @@ -291,6 +292,34 @@ class RelatedManager: self.value = value self.instance.__dict__[self.field.name] = value + @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 + def _get_queryset(self): model = apps.get_model(self.field.to) value = self.value @@ -303,20 +332,43 @@ class RelatedManager: return model.objects.filter(id__in=value["ids"]) elif value["type"] == "attrs" and isinstance(value.get("attrs"), list): filters = Q() + excludes = Q() for attr in value["attrs"]: if not isinstance(attr, dict): continue + name = attr.get('name') val = attr.get('value') match = attr.get('match', 'exact') + rel = attr.get('rel', 'and') if name is None or val is None: continue - lookup = name - if match in ("exact", "contains", "startswith", "endswith", "regex"): + if val == '*': + filters = Q() + break + + if match == 'ip_in': + q = self.get_ip_in_q(name, val) + elif match in ("exact", "contains", "startswith", "endswith", "regex"): lookup = "{}__{}".format(name, match) - filters &= Q(**{lookup: val}) - return model.objects.filter(filters) + q = Q(**{lookup: val}) + elif match == "in" and isinstance(val, list): + if '*' not in val: + lookup = "{}__in".format(name) + q = Q(**{lookup: val}) + else: + q = Q() + else: + q = Q(**{name: val}) + + if rel == 'or': + filters |= q + elif rel == 'not': + excludes |= q + else: + filters &= q + return model.objects.filter(filters).exclude(excludes) else: return model.objects.none() @@ -401,19 +453,16 @@ class JSONManyToManyField(models.JSONField): if 'name' not in attr or 'value' not in attr: raise e - def get_db_prep_value(self, manager, connection, prepared=False): - if manager is None: - return None - v = manager.value - self._check_value(v) - return json.dumps(v) + def get_db_prep_value(self, value, connection, prepared=False): + return self.get_prep_value(value) - def get_prep_value(self, manager): - if manager is None: - return manager - v = manager.value - self._check_value(v) - return json.dumps(v) + def get_prep_value(self, value): + if value is None: + return None + if isinstance(value, RelatedManager): + value = value.value + self._check_value(value) + return json.dumps(value) def validate(self, value, model_instance): super().validate(value, model_instance)