From 8f699fa366c86903e03c482fa9ba0031b54b88d4 Mon Sep 17 00:00:00 2001
From: ibuler <ibuler@qq.com>
Date: Sun, 30 Jun 2019 20:10:34 +0800
Subject: [PATCH] =?UTF-8?q?[Update]=20=E4=BF=AE=E6=94=B9Permission?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/perms/forms/asset_permission.py          |  27 +++-
 .../migrations/0006_auto_20190628_1921.py     |   5 +-
 apps/perms/models/asset_permission.py         |  39 ++++--
 apps/perms/serializers/asset_permission.py    |  26 +++-
 .../perms/asset_permission_create_update.html |  19 ++-
 .../perms/asset_permission_list.html          |   4 +-
 apps/perms/utils/asset_permission.py          | 126 ++++++++----------
 apps/perms/utils/test_asset_permission.py     |  36 +++++
 apps/perms/views/asset_permission.py          |   4 +-
 9 files changed, 183 insertions(+), 103 deletions(-)
 create mode 100644 apps/perms/utils/test_asset_permission.py

diff --git a/apps/perms/forms/asset_permission.py b/apps/perms/forms/asset_permission.py
index d1fc76383..402e50da6 100644
--- a/apps/perms/forms/asset_permission.py
+++ b/apps/perms/forms/asset_permission.py
@@ -7,15 +7,36 @@ from django.utils.translation import ugettext_lazy as _
 
 from orgs.mixins import OrgModelForm
 from orgs.utils import current_org
-from perms.models import AssetPermission
 from assets.models import Asset, Node
+from ..models import AssetPermission, ActionFlag
 
 __all__ = [
     'AssetPermissionForm',
 ]
 
 
+class ActionField(forms.MultipleChoiceField):
+    def __init__(self, *args, **kwargs):
+        kwargs['choices'] = ActionFlag.CHOICES
+        kwargs['initial'] = ActionFlag.ALL
+        kwargs['label'] = _("Action")
+        kwargs['widget'] = forms.CheckboxSelectMultiple()
+        super().__init__(*args, **kwargs)
+
+    def to_python(self, value):
+        value = super().to_python(value)
+        return ActionFlag.choices_to_value(value)
+
+    def prepare_value(self, value):
+        if value is None:
+            return value
+        value = ActionFlag.value_to_choices(value)
+        return value
+
+
 class AssetPermissionForm(OrgModelForm):
+    action = ActionField()
+
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         users_field = self.fields.get('users')
@@ -32,10 +53,6 @@ class AssetPermissionForm(OrgModelForm):
             nodes_field = self.fields['nodes']
             nodes_field._queryset = Node.get_queryset()
 
-    def clean_action(self):
-        actions = self.cleaned_data.get("action")
-        return reduce(lambda x, y: x | y, actions)
-
     class Meta:
         model = AssetPermission
         exclude = (
diff --git a/apps/perms/migrations/0006_auto_20190628_1921.py b/apps/perms/migrations/0006_auto_20190628_1921.py
index 764814e29..53afee94f 100644
--- a/apps/perms/migrations/0006_auto_20190628_1921.py
+++ b/apps/perms/migrations/0006_auto_20190628_1921.py
@@ -36,10 +36,7 @@ class Migration(migrations.Migration):
         migrations.AddField(
             model_name='assetpermission',
             name='action',
-            field=models.IntegerField(
-                choices=[(255, 'All'), (1, 'Connect'), (2, 'Upload file'),
-                         (6, 'Upload download'), (4, 'Download file')],
-                default=255, verbose_name='Action'),
+            field=models.IntegerField(choices=[(255, 'All'), (1, 'Connect'), (2, 'Upload file'), (5, 'Upload download'), (4, 'Download file')], default=255, verbose_name='Action'),
         ),
         migrations.RunPython(migrate_old_actions),
     ]
diff --git a/apps/perms/models/asset_permission.py b/apps/perms/models/asset_permission.py
index a1c998daa..16d2889a1 100644
--- a/apps/perms/models/asset_permission.py
+++ b/apps/perms/models/asset_permission.py
@@ -1,4 +1,5 @@
 import uuid
+from functools import reduce
 
 from django.db import models
 from django.utils.translation import ugettext_lazy as _
@@ -37,17 +38,41 @@ class ActionFlag:
     CONNECT = 0b00000001
     UPLOAD = 0b00000010
     DOWNLOAD = 0b00000100
-    UPDOWNLOAD = CONNECT | DOWNLOAD
+    UPDOWNLOAD = UPLOAD | DOWNLOAD
+    CONNECT_UPLOADOWN = CONNECT | UPDOWNLOAD
     ALL = 0b11111111
+    NAME_MAP = {
+        "connect": CONNECT,
+        "upload": UPLOAD,
+        "download": DOWNLOAD,
+        "updownload": UPDOWNLOAD,
+        "all": ALL,
+    }
 
     CHOICES = (
         (ALL, _('All')),
         (CONNECT, _('Connect')),
-        (UPLOAD, _('Upload file')),
         (UPDOWNLOAD, _("Upload download")),
+        (UPLOAD, _('Upload file')),
         (DOWNLOAD, _('Download file')),
     )
 
+    @classmethod
+    def value_to_choices(cls, value):
+        value = int(value)
+        if value == cls.ALL:
+            return [cls.ALL]
+        elif value == cls.UPDOWNLOAD:
+            return [cls.UPDOWNLOAD]
+        elif value == cls.CONNECT_UPLOADOWN:
+            return [cls.CONNECT, cls.UPDOWNLOAD]
+        else:
+            return [i for i in dict(cls.CHOICES) if i == i & int(value)]
+
+    @classmethod
+    def choices_to_value(cls, value):
+        return reduce(lambda x, y: int(x) | int(y), value)
+
 
 class AssetPermission(BasePermission):
     assets = models.ManyToManyField('assets.Asset', related_name='granted_by_permissions', blank=True, verbose_name=_("Asset"))
@@ -60,13 +85,9 @@ class AssetPermission(BasePermission):
         unique_together = [('org_id', 'name')]
         verbose_name = _("Asset permission")
 
-    def get_all_assets(self):
-        assets = set(self.assets.all())
-        for node in self.nodes.all():
-            _assets = node.get_all_assets()
-            set_or_append_attr_bulk(_assets, 'inherit', node.value)
-            assets.update(set(_assets))
-        return assets
+    @classmethod
+    def get_queryset_with_prefetch(cls):
+        return cls.objects.all().valid().prefetch_related('nodes', 'assets', 'system_users')
 
 
 class NodePermission(OrgModelMixin):
diff --git a/apps/perms/serializers/asset_permission.py b/apps/perms/serializers/asset_permission.py
index f1a1add19..b3f076d1d 100644
--- a/apps/perms/serializers/asset_permission.py
+++ b/apps/perms/serializers/asset_permission.py
@@ -1,11 +1,12 @@
 # -*- coding: utf-8 -*-
 #
 
+from functools import reduce
 from rest_framework import serializers
 
 from common.fields import StringManyToManyField
 from orgs.mixins import BulkOrgResourceModelSerializer
-from perms.models import AssetPermission, Action
+from perms.models import AssetPermission, Action, ActionFlag
 from assets.models import Node
 from assets.serializers import AssetGrantedSerializer
 
@@ -17,7 +18,28 @@ __all__ = [
 ]
 
 
+class ActionField(serializers.MultipleChoiceField):
+    def __init__(self, *args, **kwargs):
+        kwargs['choices'] = ActionFlag.CHOICES
+        super().__init__(*args, **kwargs)
+
+    def to_representation(self, value):
+        return ActionFlag.value_to_choices(value)
+
+    def to_internal_value(self, data):
+        return ActionFlag.choices_to_value(data)
+
+
+class ActionDisplayField(ActionField):
+    def to_representation(self, value):
+        values = super().to_representation(value)
+        choices = dict(ActionFlag.CHOICES)
+        return [choices.get(i) for i in values]
+
+
 class AssetPermissionCreateUpdateSerializer(BulkOrgResourceModelSerializer):
+    action = ActionField()
+
     class Meta:
         model = AssetPermission
         exclude = ('created_by', 'date_created')
@@ -29,7 +51,7 @@ class AssetPermissionListSerializer(BulkOrgResourceModelSerializer):
     assets = StringManyToManyField(many=True, read_only=True)
     nodes = StringManyToManyField(many=True, read_only=True)
     system_users = StringManyToManyField(many=True, read_only=True)
-    action = serializers.IntegerField(read_only=True)
+    action = ActionDisplayField()
     is_valid = serializers.BooleanField()
     is_expired = serializers.BooleanField()
 
diff --git a/apps/perms/templates/perms/asset_permission_create_update.html b/apps/perms/templates/perms/asset_permission_create_update.html
index b9945defe..af7157efc 100644
--- a/apps/perms/templates/perms/asset_permission_create_update.html
+++ b/apps/perms/templates/perms/asset_permission_create_update.html
@@ -110,6 +110,7 @@ var dateOptions = {
         format: 'YYYY-MM-DD HH:mm'
     }
 };
+var api_action =  "{{ api_action }}";
 $(document).ready(function () {
     $('.select2').select2({
         closeOnSelect: false
@@ -147,21 +148,17 @@ $(document).ready(function () {
 .on("submit", "form", function (evt) {
     evt.preventDefault();
     var the_url = '{% url 'api-perms:asset-permission-list' %}';
-    var redirect_to = '{% url "perms:asset-permission-list" %}';
     var method = "POST";
+    {% if api_action == "update" %}
+        the_url = '{% url 'api-perms:asset-permission-detail' pk=object.id %}';
+        method = "PUT";
+    {% endif %}
+    var redirect_to = '{% url "perms:asset-permission-list" %}';
     var form = $("form");
     var data = form.serializeObject();
-    console.log(data)
-    var actions = data.action;
-    var action = 0;
-    for (i=0;i<actions.length;i++) {
-        console.log(actions[i])
-        action |= actions[i];
-    }
-    data.action = action;
-    objectAttrsIsList(data, ['users', 'user_groups', 'system_users', 'nodes', 'assets']);
+    objectAttrsIsList(data, ['users', 'user_groups', 'system_users', 'nodes', 'assets', 'actions']);
     objectAttrsIsDatetime(data, ['date_start', 'date_expired']);
-    objectAttrsIsBool(data, ['is_active'])
+    objectAttrsIsBool(data, ['is_active']);
     console.log(data)
     var props = {
         url: the_url,
diff --git a/apps/perms/templates/perms/asset_permission_list.html b/apps/perms/templates/perms/asset_permission_list.html
index b8c5b3af9..afd68648b 100644
--- a/apps/perms/templates/perms/asset_permission_list.html
+++ b/apps/perms/templates/perms/asset_permission_list.html
@@ -122,8 +122,8 @@ function format(d) {
     if (d.system_users.length > 0) {
         data += makeLabel(["{% trans 'System user' %}", d.system_users.join(", ")])
     }
-    if (d.actions.length > 0) {
-        data += makeLabel(["{% trans 'Action' %}", d.actions.join(", ")])
+    if (d.action.length > 0) {
+        data += makeLabel(["{% trans 'Action' %}", d.action.join(", ")])
     }
     return data
 }
diff --git a/apps/perms/utils/asset_permission.py b/apps/perms/utils/asset_permission.py
index 3bd337004..9df402b6e 100644
--- a/apps/perms/utils/asset_permission.py
+++ b/apps/perms/utils/asset_permission.py
@@ -103,11 +103,11 @@ def get_user_permissions(user, include_group=True):
         arg = Q(users=user) | Q(user_groups__in=groups)
     else:
         arg = Q(users=user)
-    return AssetPermission.objects.valid().filter(arg)
+    return AssetPermission.get_queryset_with_prefetch().filter(arg)
 
 
 def get_user_group_permissions(user_group):
-    return AssetPermission.objects.valid().filter(
+    return AssetPermission.get_queryset_with_prefetch().filter(
         user_groups=user_group
     )
 
@@ -282,36 +282,55 @@ class AssetPermissionCacheMixin:
         cache.delete_pattern(key)
 
 
-class FlatPermissionQueryset:
-    def __init__(self):
-        self.queryset = defaultdict(list)
-
-    def add(self, permission):
-        self.queryset[permission.id].append(permission)
-
-    def add_many(self, assets_or_nodes, system_users, actions):
-        if any([assets_or_nodes, system_users, actions]):
+class FlatPermissionQueryset(set):
+    def add_many(self, assets_or_nodes, system_users, action, rtp="asset"):
+        print("Add many: {}-{}-{}".format(len(assets_or_nodes), len(system_users), action))
+        if not any([assets_or_nodes, system_users, action]):
             return
 
-        iterable = itertools.product(assets_or_nodes, system_users, actions)
+        iterable = itertools.product(assets_or_nodes, system_users, [action])
         for source, sysuser, action in iterable:
-            permission = FlatPermission(source, sysuser, action)
+            permission = FlatPermission(source, sysuser, action, rtp=rtp)
+            print("ADDDDDDDDDDDDDDDd")
             self.add(permission)
 
-    def clean(self):
-        pass
+    def group_by_resource(self):
+        resources = defaultdict(lambda: defaultdict(int))
+        for i in self:
+            resources[i.resource][i.system_user] |= i.action
+        return resources
 
 
 class FlatPermission:
-    def __init__(self, asset_or_node, system_user, action):
-        self.id = asset_or_node.id
-        self.source = asset_or_node
+    def __init__(self, assets_or_node, system_user, action, rtp="asset"):
+        self.id = "{}_{}_{}".format(assets_or_node.id, system_user.id, action)
+        self.resource = assets_or_node
+        self.resource_type = rtp
         self.system_user = system_user
         self.action = action
 
     def __eq__(self, other):
-        pass
+        if self.id == other.id:
+            return True
+        # 资产不同
+        if self.resource_type == "asset" and self.id != other.id:
+            return False
+        # 不是子节点
+        elif self.resource_type == "node" and not other.resource.key.startswith(self.resource.key):
+            return False
+        # 系统用户优先级大于后者,则相同
+        if self.system_user.priority > self.system_user.priority:
+            return True
+        # 如果系统用户不同,则不同
+        elif self.system_user != other.system_user:
+            return False
+        # 如果action为与后的结果则相同
+        if self.action == self.action | other.action:
+            return True
+        return False
 
+    def __hash__(self):
+        return hash(self.id)
 
 
 class AssetPermissionUtil(AssetPermissionCacheMixin):
@@ -355,33 +374,20 @@ class AssetPermissionUtil(AssetPermissionCacheMixin):
         self._permissions = self.permissions.filter(**filters)
         self._filter_id = md5(filters_json.encode()).hexdigest()
 
-    @staticmethod
-    @timeit
-    def _structured_system_user(system_users, actions):
-        """
-        结构化系统用户
-        :param system_users:
-        :param actions:
-        :return: {system_user1: {'actions': set(), }, }
-        """
-        _attr = {'actions': set(actions)}
-        _system_users = {system_user: _attr for system_user in system_users}
-        return _system_users
-
     @timeit
     def get_nodes_direct(self):
         """
         返回用户/组授权规则直接关联的节点
         :return: {node1: {system_user1: {'actions': set()},}}
         """
-        nodes = FlatPermissionQueryset()
-        permissions = self.permissions
-        for perm in permissions:
-            actions = perm.actions.all()
+        queryset = FlatPermissionQueryset()
+        for perm in self.permissions:
+            actions = perm.action
             system_users = perm.system_users.all()
-            _nodes = perm.nodes.all()
-            nodes.add_many(_nodes, system_users, actions)
-        return nodes
+            nodes = perm.nodes.all()
+            queryset.add_many(nodes, system_users, actions, rtp="nodes")
+        print(queryset)
+        return queryset.group_by_resource()
 
     @timeit
     def get_assets_direct(self):
@@ -389,15 +395,14 @@ class AssetPermissionUtil(AssetPermissionCacheMixin):
         返回用户授权规则直接关联的资产
         :return: {asset1: {system_user1: {'actions': set()},}}
         """
-        assets = defaultdict(dict)
-        permissions = self.permissions.prefetch_related('assets', 'system_users')
-        for perm in permissions:
-            actions = perm.actions.all()
-            for asset in perm.assets.all().valid().prefetch_related('nodes'):
-                system_users = perm.system_users.filter(protocol__in=asset.protocols_name)
-                system_users = self._structured_system_user(system_users, actions)
-                assets[asset].update(system_users)
-        return assets
+        queryset = FlatPermissionQueryset()
+        for perm in self.permissions:
+            action = perm.action
+            assets = perm.assets.all()
+            system_users = perm.system_users.all()
+            queryset.add_many(assets, system_users, action, rtp="assets")
+        print(queryset)
+        return queryset.group_by_resource()
 
     @timeit
     def get_assets_without_cache(self):
@@ -408,27 +413,10 @@ class AssetPermissionUtil(AssetPermissionCacheMixin):
             return self._assets
         assets = self.get_assets_direct()
         nodes = self.get_nodes_direct()
-        # for node, system_users in nodes.items():
-        #     print(">>>>> Node<<<<<<<<<<<<: ", node.value)
-        #     _assets = list(node.get_all_valid_assets())
-        #     for asset in _assets:
-        #         for system_user, attr_dict in system_users.items():
-        #             if not asset.has_protocol(system_user.protocol):
-        #                 continue
-        #             if system_user in assets[asset]:
-        #                 actions = assets[asset][system_user]['actions']
-        #                 attr_dict['actions'].update(actions)
-        #                 system_users.update({system_user: attr_dict})
-        #         assets[asset].update(system_users)
-
-        __assets = defaultdict(set)
-        for asset, system_users in assets.items():
-            for system_user, attr_dict in system_users.items():
-                setattr(system_user, 'actions', attr_dict['actions'])
-            __assets[asset] = set(system_users.keys())
-
-        self._assets = __assets
-        return self._assets
+        print("++++++++++++++++++++++")
+        print(assets)
+        print("---------------------")
+        print(nodes)
 
     @timeit
     def get_nodes_with_assets_without_cache(self):
diff --git a/apps/perms/utils/test_asset_permission.py b/apps/perms/utils/test_asset_permission.py
new file mode 100644
index 000000000..d4686f803
--- /dev/null
+++ b/apps/perms/utils/test_asset_permission.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+#
+from django.test import TestCase
+
+from assets.models import Node, SystemUser
+from .asset_permission import FlatPermission
+from ..models import ActionFlag
+
+
+class TestFlatPermissionEqual(TestCase):
+    def setUp(self):
+        node1 = Node(value="parent", key="1:1")
+        node2 = Node(value="child", key="1:1:1")
+
+        system_user1 = SystemUser(username="name1", name="name1", priority=20)
+        system_user2 = SystemUser(username="name2", name="name2", priority=10)
+
+        action1 = ActionFlag.ALL
+        action2 = ActionFlag.CONNECT
+        action3 = ActionFlag.UPDOWNLOAD
+
+        perm1 = FlatPermission(node1, system_user1, action1)
+        perm2 = FlatPermission(node2, system_user1, action1)
+        perm3 = FlatPermission(node2, system_user2, action1)
+
+        self.groups = (
+            (perm1, perm2, True),
+            (perm1, perm3, True),
+        )
+
+    def test_equal(self):
+        for k, k2, wanted in self.groups:
+            if (k == k2) != wanted:
+                print("Not equal {} {}", k, k2)
+
+
diff --git a/apps/perms/views/asset_permission.py b/apps/perms/views/asset_permission.py
index 7acda41ef..e354e7479 100644
--- a/apps/perms/views/asset_permission.py
+++ b/apps/perms/views/asset_permission.py
@@ -64,6 +64,7 @@ class AssetPermissionCreateView(PermissionsMixin, CreateView):
         context = {
             'app': _('Perms'),
             'action': _('Create asset permission'),
+            'api_action': "create",
         }
         kwargs.update(context)
         return super().get_context_data(**kwargs)
@@ -79,7 +80,8 @@ class AssetPermissionUpdateView(PermissionsMixin, UpdateView):
     def get_context_data(self, **kwargs):
         context = {
             'app': _('Perms'),
-            'action': _('Update asset permission')
+            'action': _('Update asset permission'),
+            'api_action': "update",
         }
         kwargs.update(context)
         return super().get_context_data(**kwargs)