mirror of https://github.com/jumpserver/jumpserver
				
				
				
			perf: 修改 acl 添加命令过滤 acl
							parent
							
								
									8162a1b17e
								
							
						
					
					
						commit
						cb3877bbda
					
				| 
						 | 
				
			
			@ -0,0 +1,12 @@
 | 
			
		|||
from orgs.mixins.api import OrgBulkModelViewSet
 | 
			
		||||
from .. import models, serializers
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__all__ = ['CommandFilterACLViewSet']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CommandFilterACLViewSet(OrgBulkModelViewSet):
 | 
			
		||||
    model = models.CommandFilterACL
 | 
			
		||||
    filterset_fields = ('name', )
 | 
			
		||||
    search_fields = filterset_fields
 | 
			
		||||
    serializer_class = serializers.LoginAssetACLSerializer
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,35 @@
 | 
			
		|||
# Generated by Django 3.2.14 on 2022-12-01 10:46
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
 | 
			
		||||
        ('acls', '0004_auto_20220831_1658'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='loginacl',
 | 
			
		||||
            name='action',
 | 
			
		||||
            field=models.CharField(choices=[('reject', 'Reject'), ('allow', 'Allow'), ('confirm', 'Confirm')], default='reject', max_length=64, verbose_name='Action'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='loginacl',
 | 
			
		||||
            name='reviewers',
 | 
			
		||||
            field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Reviewers'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='loginassetacl',
 | 
			
		||||
            name='action',
 | 
			
		||||
            field=models.CharField(choices=[('reject', 'Reject'), ('allow', 'Allow'), ('confirm', 'Confirm')], default='reject', max_length=64, verbose_name='Action'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='loginassetacl',
 | 
			
		||||
            name='reviewers',
 | 
			
		||||
            field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Reviewers'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,61 @@
 | 
			
		|||
# Generated by Django 3.2.14 on 2022-12-01 11:39
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
import django.core.validators
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
import uuid
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
 | 
			
		||||
        ('acls', '0005_auto_20221201_1846'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='CommandGroup',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
 | 
			
		||||
                ('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')),
 | 
			
		||||
                ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
 | 
			
		||||
                ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
 | 
			
		||||
                ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
 | 
			
		||||
                ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
 | 
			
		||||
                ('name', models.CharField(max_length=128, verbose_name='Name')),
 | 
			
		||||
                ('type', models.CharField(choices=[('command', 'Command'), ('regex', 'Regex')], default='command', max_length=16, verbose_name='Type')),
 | 
			
		||||
                ('content', models.TextField(help_text='One line one command', verbose_name='Content')),
 | 
			
		||||
                ('ignore_case', models.BooleanField(default=True, verbose_name='Ignore case')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'Command filter rule',
 | 
			
		||||
                'unique_together': {('org_id', 'name')},
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='CommandFilterACL',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
 | 
			
		||||
                ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
 | 
			
		||||
                ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
 | 
			
		||||
                ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
 | 
			
		||||
                ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
 | 
			
		||||
                ('name', models.CharField(max_length=128, verbose_name='Name')),
 | 
			
		||||
                ('priority', models.IntegerField(default=50, help_text='1-100, the lower the value will be match first', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Priority')),
 | 
			
		||||
                ('action', models.CharField(choices=[('reject', 'Reject'), ('allow', 'Allow'), ('confirm', 'Confirm')], default='reject', max_length=64, verbose_name='Action')),
 | 
			
		||||
                ('is_active', models.BooleanField(default=True, verbose_name='Active')),
 | 
			
		||||
                ('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
 | 
			
		||||
                ('users', models.JSONField(verbose_name='User')),
 | 
			
		||||
                ('accounts', models.JSONField(verbose_name='Account')),
 | 
			
		||||
                ('assets', models.JSONField(verbose_name='Asset')),
 | 
			
		||||
                ('commands', models.ManyToManyField(to='acls.CommandGroup', verbose_name='Commands')),
 | 
			
		||||
                ('reviewers', models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Reviewers')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'Command acl',
 | 
			
		||||
                'ordering': ('priority', '-date_updated', 'name'),
 | 
			
		||||
                'unique_together': {('name', 'org_id')},
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
| 
						 | 
				
			
			@ -1,2 +1,3 @@
 | 
			
		|||
from .login_acl import *
 | 
			
		||||
from .login_asset_acl import *
 | 
			
		||||
from .command_acl import *
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,13 @@ from django.core.validators import MinValueValidator, MaxValueValidator
 | 
			
		|||
from common.mixins import CommonModelMixin
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__all__ = ['BaseACL', 'BaseACLQuerySet']
 | 
			
		||||
__all__ = ['BaseACL', 'BaseACLQuerySet', 'ACLManager']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ActionChoices(models.TextChoices):
 | 
			
		||||
    reject = 'reject', _('Reject')
 | 
			
		||||
    allow = 'allow', _('Allow')
 | 
			
		||||
    confirm = 'confirm', _('Confirm')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BaseACLQuerySet(models.QuerySet):
 | 
			
		||||
| 
						 | 
				
			
			@ -21,6 +27,11 @@ class BaseACLQuerySet(models.QuerySet):
 | 
			
		|||
        return self.inactive()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ACLManager(models.Manager):
 | 
			
		||||
    def valid(self):
 | 
			
		||||
        return self.get_queryset().valid()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BaseACL(CommonModelMixin):
 | 
			
		||||
    name = models.CharField(max_length=128, verbose_name=_('Name'))
 | 
			
		||||
    priority = models.IntegerField(
 | 
			
		||||
| 
						 | 
				
			
			@ -28,8 +39,16 @@ class BaseACL(CommonModelMixin):
 | 
			
		|||
        help_text=_("1-100, the lower the value will be match first"),
 | 
			
		||||
        validators=[MinValueValidator(1), MaxValueValidator(100)]
 | 
			
		||||
    )
 | 
			
		||||
    action = models.CharField(
 | 
			
		||||
        max_length=64, verbose_name=_('Action'),
 | 
			
		||||
        choices=ActionChoices.choices, default=ActionChoices.reject
 | 
			
		||||
    )
 | 
			
		||||
    reviewers = models.ManyToManyField('users.User', blank=True, verbose_name=_("Reviewers"))
 | 
			
		||||
    is_active = models.BooleanField(default=True, verbose_name=_("Active"))
 | 
			
		||||
    comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
 | 
			
		||||
 | 
			
		||||
    objects = ACLManager.from_queryset(BaseACLQuerySet)()
 | 
			
		||||
    ActionChoices = ActionChoices
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        abstract = True
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,162 @@
 | 
			
		|||
# -*- coding: utf-8 -*-
 | 
			
		||||
#
 | 
			
		||||
import re
 | 
			
		||||
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db.models import Q
 | 
			
		||||
from django.utils.translation import ugettext_lazy as _
 | 
			
		||||
 | 
			
		||||
from users.models import User, UserGroup
 | 
			
		||||
from orgs.mixins.models import JMSOrgBaseModel
 | 
			
		||||
from common.utils import lazyproperty, get_logger, get_object_or_none
 | 
			
		||||
from orgs.mixins.models import OrgModelMixin
 | 
			
		||||
from .base import BaseACL
 | 
			
		||||
 | 
			
		||||
logger = get_logger(__file__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CommandGroup(JMSOrgBaseModel):
 | 
			
		||||
    class Type(models.TextChoices):
 | 
			
		||||
        command = 'command', _('Command')
 | 
			
		||||
        regex = 'regex', _('Regex')
 | 
			
		||||
 | 
			
		||||
    name = models.CharField(max_length=128, verbose_name=_("Name"))
 | 
			
		||||
    type = models.CharField(max_length=16, default=Type.command, choices=Type.choices, verbose_name=_("Type"))
 | 
			
		||||
    content = models.TextField(verbose_name=_("Content"), help_text=_("One line one command"))
 | 
			
		||||
    ignore_case = models.BooleanField(default=True, verbose_name=_('Ignore case'))
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        unique_together = [('org_id', 'name')]
 | 
			
		||||
        verbose_name = _("Command filter rule")
 | 
			
		||||
 | 
			
		||||
    @lazyproperty
 | 
			
		||||
    def pattern(self):
 | 
			
		||||
        if self.type == 'command':
 | 
			
		||||
            s = self.construct_command_regex(content=self.content)
 | 
			
		||||
        else:
 | 
			
		||||
            s = r'{0}'.format(self.content)
 | 
			
		||||
        return s
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def construct_command_regex(cls, content):
 | 
			
		||||
        regex = []
 | 
			
		||||
        content = content.replace('\r\n', '\n')
 | 
			
		||||
        for _cmd in content.split('\n'):
 | 
			
		||||
            cmd = re.sub(r'\s+', ' ', _cmd)
 | 
			
		||||
            cmd = re.escape(cmd)
 | 
			
		||||
            cmd = cmd.replace('\\ ', '\s+')
 | 
			
		||||
 | 
			
		||||
            # 有空格就不能 铆钉单词了
 | 
			
		||||
            if ' ' in _cmd:
 | 
			
		||||
                regex.append(cmd)
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            if not cmd:
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            # 如果是单个字符
 | 
			
		||||
            if cmd[-1].isalpha():
 | 
			
		||||
                regex.append(r'\b{0}\b'.format(cmd))
 | 
			
		||||
            else:
 | 
			
		||||
                regex.append(r'\b{0}'.format(cmd))
 | 
			
		||||
        s = r'{}'.format('|'.join(regex))
 | 
			
		||||
        return s
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def compile_regex(regex, ignore_case):
 | 
			
		||||
        args = []
 | 
			
		||||
        if ignore_case:
 | 
			
		||||
            args.append(re.IGNORECASE)
 | 
			
		||||
        try:
 | 
			
		||||
            pattern = re.compile(regex, *args)
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            error = _('The generated regular expression is incorrect: {}').format(str(e))
 | 
			
		||||
            logger.error(error)
 | 
			
		||||
            return False, error, None
 | 
			
		||||
        return True, '', pattern
 | 
			
		||||
 | 
			
		||||
    def match(self, data):
 | 
			
		||||
        succeed, error, pattern = self.compile_regex(self.pattern, self.ignore_case)
 | 
			
		||||
        if not succeed:
 | 
			
		||||
            return False, ''
 | 
			
		||||
 | 
			
		||||
        found = pattern.search(data)
 | 
			
		||||
        if not found:
 | 
			
		||||
            return False, ''
 | 
			
		||||
        else:
 | 
			
		||||
            return True, found.group()
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return '{} % {}'.format(self.type, self.content)
 | 
			
		||||
 | 
			
		||||
    def create_command_confirm_ticket(self, run_command, session, cmd_filter_rule, org_id):
 | 
			
		||||
        from tickets.const import TicketType
 | 
			
		||||
        from tickets.models import ApplyCommandTicket
 | 
			
		||||
        data = {
 | 
			
		||||
            'title': _('Command confirm') + ' ({})'.format(session.user),
 | 
			
		||||
            'type': TicketType.command_confirm,
 | 
			
		||||
            'applicant': session.user_obj,
 | 
			
		||||
            'apply_run_user_id': session.user_id,
 | 
			
		||||
            'apply_run_asset': str(session.asset),
 | 
			
		||||
            'apply_run_account': str(session.account),
 | 
			
		||||
            'apply_run_command': run_command[:4090],
 | 
			
		||||
            'apply_from_session_id': str(session.id),
 | 
			
		||||
            'apply_from_cmd_filter_rule_id': str(cmd_filter_rule.id),
 | 
			
		||||
            'apply_from_cmd_filter_id': str(cmd_filter_rule.filter.id),
 | 
			
		||||
            'org_id': org_id,
 | 
			
		||||
        }
 | 
			
		||||
        ticket = ApplyCommandTicket.objects.create(**data)
 | 
			
		||||
        assignees = self.reviewers.all()
 | 
			
		||||
        ticket.open_by_system(assignees)
 | 
			
		||||
        return ticket
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def get_queryset(
 | 
			
		||||
            cls, user_id=None, user_group_id=None, account=None,
 | 
			
		||||
            asset_id=None, org_id=None
 | 
			
		||||
    ):
 | 
			
		||||
        from assets.models import Account
 | 
			
		||||
        user_groups = []
 | 
			
		||||
        user = get_object_or_none(User, pk=user_id)
 | 
			
		||||
        if user:
 | 
			
		||||
            user_groups.extend(list(user.groups.all()))
 | 
			
		||||
        user_group = get_object_or_none(UserGroup, pk=user_group_id)
 | 
			
		||||
        if user_group:
 | 
			
		||||
            org_id = user_group.org_id
 | 
			
		||||
            user_groups.append(user_group)
 | 
			
		||||
 | 
			
		||||
        asset = get_object_or_none(Asset, pk=asset_id)
 | 
			
		||||
        q = Q()
 | 
			
		||||
        if user:
 | 
			
		||||
            q |= Q(users=user)
 | 
			
		||||
        if user_groups:
 | 
			
		||||
            q |= Q(user_groups__in=set(user_groups))
 | 
			
		||||
        if account:
 | 
			
		||||
            org_id = account.org_id
 | 
			
		||||
            q |= Q(accounts__contains=account.username) | \
 | 
			
		||||
                 Q(accounts__contains=Account.AliasAccount.ALL)
 | 
			
		||||
        if asset:
 | 
			
		||||
            org_id = asset.org_id
 | 
			
		||||
            q |= Q(assets=asset)
 | 
			
		||||
        if q:
 | 
			
		||||
            cmd_filters = CommandFilter.objects.filter(q).filter(is_active=True)
 | 
			
		||||
            if org_id:
 | 
			
		||||
                cmd_filters = cmd_filters.filter(org_id=org_id)
 | 
			
		||||
            rule_ids = cmd_filters.values_list('rules', flat=True)
 | 
			
		||||
            rules = cls.objects.filter(id__in=rule_ids)
 | 
			
		||||
        else:
 | 
			
		||||
            rules = cls.objects.none()
 | 
			
		||||
        return rules
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CommandFilterACL(OrgModelMixin, BaseACL):
 | 
			
		||||
    # 条件
 | 
			
		||||
    users = models.JSONField(verbose_name=_('User'))
 | 
			
		||||
    accounts = models.JSONField(verbose_name=_('Account'))
 | 
			
		||||
    assets = models.JSONField(verbose_name=_('Asset'))
 | 
			
		||||
    commands = models.ManyToManyField(CommandGroup, verbose_name=_('Commands'))
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        unique_together = ('name', 'org_id')
 | 
			
		||||
        ordering = ('priority', '-date_updated', 'name')
 | 
			
		||||
        verbose_name = _('Command acl')
 | 
			
		||||
| 
						 | 
				
			
			@ -1,24 +1,14 @@
 | 
			
		|||
from django.db import models
 | 
			
		||||
from django.utils.translation import ugettext_lazy as _
 | 
			
		||||
from .base import BaseACL, BaseACLQuerySet
 | 
			
		||||
 | 
			
		||||
from common.utils import get_request_ip, get_ip_city
 | 
			
		||||
from common.utils.ip import contains_ip
 | 
			
		||||
from common.utils.time_period import contains_time_period
 | 
			
		||||
from common.utils.timezone import local_now_display
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ACLManager(models.Manager):
 | 
			
		||||
 | 
			
		||||
    def valid(self):
 | 
			
		||||
        return self.get_queryset().valid()
 | 
			
		||||
from .base import BaseACL
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LoginACL(BaseACL):
 | 
			
		||||
    class ActionChoices(models.TextChoices):
 | 
			
		||||
        reject = 'reject', _('Reject')
 | 
			
		||||
        allow = 'allow', _('Allow')
 | 
			
		||||
        confirm = 'confirm', _('Login confirm')
 | 
			
		||||
 | 
			
		||||
    # 用户
 | 
			
		||||
    user = models.ForeignKey(
 | 
			
		||||
        'users.User', on_delete=models.CASCADE, verbose_name=_('User'),
 | 
			
		||||
| 
						 | 
				
			
			@ -26,16 +16,6 @@ class LoginACL(BaseACL):
 | 
			
		|||
    )
 | 
			
		||||
    # 规则
 | 
			
		||||
    rules = models.JSONField(default=dict, verbose_name=_('Rule'))
 | 
			
		||||
    # 动作
 | 
			
		||||
    action = models.CharField(
 | 
			
		||||
        max_length=64, verbose_name=_('Action'),
 | 
			
		||||
        choices=ActionChoices.choices, default=ActionChoices.reject
 | 
			
		||||
    )
 | 
			
		||||
    reviewers = models.ManyToManyField(
 | 
			
		||||
        'users.User', verbose_name=_("Reviewers"),
 | 
			
		||||
        related_name="login_confirm_acls", blank=True
 | 
			
		||||
    )
 | 
			
		||||
    objects = ACLManager.from_queryset(BaseACLQuerySet)()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        ordering = ('priority', '-date_updated', 'name')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,7 @@ from django.db import models
 | 
			
		|||
from django.db.models import Q
 | 
			
		||||
from django.utils.translation import ugettext_lazy as _
 | 
			
		||||
from orgs.mixins.models import OrgModelMixin, OrgManager
 | 
			
		||||
from .base import BaseACL, BaseACLQuerySet
 | 
			
		||||
from .base import BaseACL, BaseACLQuerySet, ACLManager
 | 
			
		||||
from common.utils.ip import contains_ip
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -32,31 +32,11 @@ class ACLQuerySet(BaseACLQuerySet):
 | 
			
		|||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ACLManager(OrgManager):
 | 
			
		||||
 | 
			
		||||
    def valid(self):
 | 
			
		||||
        return self.get_queryset().valid()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LoginAssetACL(BaseACL, OrgModelMixin):
 | 
			
		||||
    class ActionChoices(models.TextChoices):
 | 
			
		||||
        login_confirm = 'login_confirm', _('Login confirm')
 | 
			
		||||
 | 
			
		||||
    # 条件
 | 
			
		||||
    users = models.JSONField(verbose_name=_('User'))
 | 
			
		||||
    accounts = models.JSONField(verbose_name=_('Account'))
 | 
			
		||||
    assets = models.JSONField(verbose_name=_('Asset'))
 | 
			
		||||
    # 动作
 | 
			
		||||
    action = models.CharField(
 | 
			
		||||
        max_length=64, choices=ActionChoices.choices, default=ActionChoices.login_confirm,
 | 
			
		||||
        verbose_name=_('Action')
 | 
			
		||||
    )
 | 
			
		||||
    # 动作: 附加字段
 | 
			
		||||
    # - login_confirm
 | 
			
		||||
    reviewers = models.ManyToManyField(
 | 
			
		||||
        'users.User', related_name='review_login_asset_acls', blank=True,
 | 
			
		||||
        verbose_name=_("Reviewers")
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    objects = ACLManager.from_queryset(ACLQuerySet)()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue