From bdcf9ba15332f0954fbe176fe88207e56ed4ceba Mon Sep 17 00:00:00 2001
From: BaiJiangJie <32935519+BaiJiangJie@users.noreply.github.com>
Date: Thu, 12 Sep 2019 18:25:22 +0800
Subject: [PATCH] Dev remoteapp (#3205)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* [Update] 修改RemoteApp关联的系统用户:从RemoteApp中转移到RemoteAppPermission中(未提交迁移文件)
* [Update] 修改RemoteApp关联的系统用户:提交迁移文件
* [Update] 修改RemoteApp关联的系统用户:修改迁移文件
* [Update] 修改迁移文件1
* [Update] 修改迁移文件2
* [Update] 修改迁移文件3
* [Update] 修改RemoteAppPermsUtil获取系统用户的逻辑
---
apps/applications/forms/remote_app.py | 9 +--
.../0002_remove_remoteapp_system_user.py | 18 +++++
apps/applications/models/remote_app.py | 11 ---
apps/applications/serializers/remote_app.py | 8 +-
.../remote_app_create_update.html | 1 -
.../applications/remote_app_detail.html | 4 -
.../applications/remote_app_list.html | 11 +--
.../applications/user_remote_app_list.html | 8 +-
apps/applications/urls/api_urls.py | 4 +-
apps/perms/api/mixin.py | 41 +++++++++-
apps/perms/api/user_permission.py | 30 +------
apps/perms/api/user_remote_app_permission.py | 35 +++++++-
apps/perms/forms/remote_app_permission.py | 3 +
.../0008_remoteapppermission_system_users.py | 32 ++++++++
apps/perms/models/remote_app_permission.py | 1 +
.../serializers/remote_app_permission.py | 4 +-
apps/perms/serializers/user_permission.py | 14 +++-
.../remote_app_permission_create_update.html | 3 +-
.../perms/remote_app_permission_detail.html | 80 ++++++++++++++++++-
apps/perms/urls/api_urls.py | 4 +
apps/perms/utils/remote_app_permission.py | 12 ++-
apps/perms/views/remote_app_permission.py | 5 +-
22 files changed, 250 insertions(+), 88 deletions(-)
create mode 100644 apps/applications/migrations/0002_remove_remoteapp_system_user.py
create mode 100644 apps/perms/migrations/0008_remoteapppermission_system_users.py
diff --git a/apps/applications/forms/remote_app.py b/apps/applications/forms/remote_app.py
index 81fc20b2b..b12759462 100644
--- a/apps/applications/forms/remote_app.py
+++ b/apps/applications/forms/remote_app.py
@@ -89,23 +89,16 @@ class RemoteAppCreateUpdateForm(RemoteAppTypeForms, OrgModelForm):
super().__init__(*args, **kwargs)
field_asset = self.fields['asset']
field_asset.queryset = field_asset.queryset.has_protocol('rdp')
- field_system_user = self.fields['system_user']
- field_system_user.queryset = field_system_user.queryset.filter(
- protocol=SystemUser.PROTOCOL_RDP
- )
class Meta:
model = RemoteApp
fields = [
- 'name', 'asset', 'system_user', 'type', 'path', 'comment'
+ 'name', 'asset', 'type', 'path', 'comment'
]
widgets = {
'asset': forms.Select(attrs={
'class': 'select2', 'data-placeholder': _('Asset')
}),
- 'system_user': forms.Select(attrs={
- 'class': 'select2', 'data-placeholder': _('System user')
- })
}
def _clean_params(self):
diff --git a/apps/applications/migrations/0002_remove_remoteapp_system_user.py b/apps/applications/migrations/0002_remove_remoteapp_system_user.py
new file mode 100644
index 000000000..31d497f57
--- /dev/null
+++ b/apps/applications/migrations/0002_remove_remoteapp_system_user.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.1.7 on 2019-09-09 09:57
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('applications', '0001_initial'),
+ ('perms', '0008_remoteapppermission_system_users'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='remoteapp',
+ name='system_user',
+ ),
+ ]
diff --git a/apps/applications/models/remote_app.py b/apps/applications/models/remote_app.py
index 636eb1f66..17746833c 100644
--- a/apps/applications/models/remote_app.py
+++ b/apps/applications/models/remote_app.py
@@ -22,10 +22,6 @@ class RemoteApp(OrgModelMixin):
asset = models.ForeignKey(
'assets.Asset', on_delete=models.CASCADE, verbose_name=_('Asset')
)
- system_user = models.ForeignKey(
- 'assets.SystemUser', on_delete=models.CASCADE,
- verbose_name=_('System user')
- )
type = models.CharField(
default=const.REMOTE_APP_TYPE_CHROME,
choices=const.REMOTE_APP_TYPE_CHOICES,
@@ -80,10 +76,3 @@ class RemoteApp(OrgModelMixin):
'id': self.asset.id,
'hostname': self.asset.hostname
}
-
- @property
- def system_user_info(self):
- return {
- 'id': self.system_user.id,
- 'name': self.system_user.name
- }
diff --git a/apps/applications/serializers/remote_app.py b/apps/applications/serializers/remote_app.py
index 80faff539..168a02280 100644
--- a/apps/applications/serializers/remote_app.py
+++ b/apps/applications/serializers/remote_app.py
@@ -73,13 +73,13 @@ class RemoteAppSerializer(BulkOrgResourceModelSerializer):
model = RemoteApp
list_serializer_class = AdaptedBulkListSerializer
fields = [
- 'id', 'name', 'asset', 'system_user', 'type', 'path', 'params',
+ 'id', 'name', 'asset', 'type', 'path', 'params',
'comment', 'created_by', 'date_created', 'asset_info',
- 'system_user_info', 'get_type_display',
+ 'get_type_display',
]
read_only_fields = [
'created_by', 'date_created', 'asset_info',
- 'system_user_info', 'get_type_display'
+ 'get_type_display'
]
@@ -89,7 +89,7 @@ class RemoteAppConnectionInfoSerializer(serializers.ModelSerializer):
class Meta:
model = RemoteApp
fields = [
- 'id', 'name', 'asset', 'system_user', 'parameter_remote_app',
+ 'id', 'name', 'asset', 'parameter_remote_app',
]
read_only_fields = ['parameter_remote_app']
diff --git a/apps/applications/templates/applications/remote_app_create_update.html b/apps/applications/templates/applications/remote_app_create_update.html
index 246479fb3..b193dfff5 100644
--- a/apps/applications/templates/applications/remote_app_create_update.html
+++ b/apps/applications/templates/applications/remote_app_create_update.html
@@ -13,7 +13,6 @@
{% csrf_token %}
{% bootstrap_field form.name layout="horizontal" %}
{% bootstrap_field form.asset layout="horizontal" %}
- {% bootstrap_field form.system_user layout="horizontal" %}
{% bootstrap_field form.type layout="horizontal" %}
{% bootstrap_field form.path layout="horizontal" %}
diff --git a/apps/applications/templates/applications/remote_app_detail.html b/apps/applications/templates/applications/remote_app_detail.html
index d006bb51a..19105da89 100644
--- a/apps/applications/templates/applications/remote_app_detail.html
+++ b/apps/applications/templates/applications/remote_app_detail.html
@@ -57,10 +57,6 @@
{% trans 'Asset' %}: |
{{ remote_app.asset.hostname }} |
-
- {% trans 'System user' %}: |
- {{ remote_app.system_user.name }} |
-
{% trans 'App type' %}: |
{{ remote_app.get_type_display }} |
diff --git a/apps/applications/templates/applications/remote_app_list.html b/apps/applications/templates/applications/remote_app_list.html
index 54f1806e9..3dbe4f8eb 100644
--- a/apps/applications/templates/applications/remote_app_list.html
+++ b/apps/applications/templates/applications/remote_app_list.html
@@ -20,7 +20,6 @@
{% trans 'Name' %} |
{% trans 'App type' %} |
{% trans 'Asset' %} |
- {% trans 'System user' %} |
{% trans 'Comment' %} |
{% trans 'Action' %} |
@@ -47,12 +46,11 @@ function initTable() {
var detail_btn = '' + hostname + '';
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', cellData.id));
}},
- {targets: 4, createdCell: function (td, cellData, rowData) {
- var name = htmlEscape(cellData.name);
- var detail_btn = '' + name + '';
- $(td).html(detail_btn.replace('{{ DEFAULT_PK }}', cellData.id));
+ {targets: 3, createdCell: function (td, cellData, rowData) {
+ var comment = htmlEscape(cellData);
+ $(td).html(comment)
}},
- {targets: 6, createdCell: function (td, cellData, rowData) {
+ {targets: 5, createdCell: function (td, cellData, rowData) {
var update_btn = '{% trans "Update" %}'.replace("{{ DEFAULT_PK }}", cellData);
var del_btn = '{% trans "Delete" %}'.replace('{{ DEFAULT_PK }}', cellData);
$(td).html(update_btn + del_btn)
@@ -64,7 +62,6 @@ function initTable() {
{data: "name" },
{data: "get_type_display", orderable: false},
{data: "asset_info", orderable: false},
- {data: "system_user_info", orderable: false},
{data: "comment"},
{data: "id", orderable: false}
],
diff --git a/apps/applications/templates/applications/user_remote_app_list.html b/apps/applications/templates/applications/user_remote_app_list.html
index 4199e805f..3559232ef 100644
--- a/apps/applications/templates/applications/user_remote_app_list.html
+++ b/apps/applications/templates/applications/user_remote_app_list.html
@@ -16,7 +16,6 @@
{% trans 'Name' %} |
{% trans 'App type' %} |
{% trans 'Asset' %} |
- {% trans 'System user' %} |
{% trans 'Comment' %} |
{% trans 'Action' %} |
@@ -49,11 +48,7 @@ function initTable() {
var hostname = htmlEscape(cellData.hostname);
$(td).html(hostname);
}},
- {targets: 4, createdCell: function (td, cellData, rowData) {
- var name = htmlEscape(cellData.name);
- $(td).html(name);
- }},
- {targets: 6, createdCell: function (td, cellData, rowData) {
+ {targets: 5, createdCell: function (td, cellData, rowData) {
var conn_btn = '{% trans "Connect" %}'.replace("{{ DEFAULT_PK }}", cellData);
$(td).html(conn_btn)
}}
@@ -64,7 +59,6 @@ function initTable() {
{data: "name"},
{data: "get_type_display", orderable: false},
{data: "asset_info", orderable: false},
- {data: "system_user_info", orderable: false},
{data: "comment", orderable: false},
{data: "id", orderable: false}
]
diff --git a/apps/applications/urls/api_urls.py b/apps/applications/urls/api_urls.py
index 137cea733..6384f5dac 100644
--- a/apps/applications/urls/api_urls.py
+++ b/apps/applications/urls/api_urls.py
@@ -13,9 +13,7 @@ router = BulkRouter()
router.register(r'remote-apps', api.RemoteAppViewSet, 'remote-app')
urlpatterns = [
- path('remote-apps//connection-info/',
- api.RemoteAppConnectionInfoApi.as_view(),
- name='remote-app-connection-info')
+ path('remote-apps//connection-info/', api.RemoteAppConnectionInfoApi.as_view(), name='remote-app-connection-info'),
]
old_version_urlpatterns = [
re_path('(?Premote-app)/.*', capi.redirect_plural_name_api)
diff --git a/apps/perms/api/mixin.py b/apps/perms/api/mixin.py
index 2227857ca..e2cdb139b 100644
--- a/apps/perms/api/mixin.py
+++ b/apps/perms/api/mixin.py
@@ -9,16 +9,49 @@ from rest_framework.views import Response
from django.utils.decorators import method_decorator
from django.views.decorators.http import condition
+from rest_framework.generics import get_object_or_404
from django.utils.translation import ugettext as _
-from common.utils import get_logger
from assets.utils import LabelFilterMixin
-from .. import const
-from ..hands import Asset, Node, SystemUser
+from common.permissions import IsValidUser, IsOrgAdminOrAppUser, IsOrgAdmin
+from common.utils import get_logger
+from orgs.utils import set_to_root_org
+from ..hands import User, Asset, Node, SystemUser
from .. import serializers
+from .. import const
+
logger = get_logger(__name__)
-__all__ = ['UserPermissionCacheMixin', 'GrantAssetsMixin', 'NodesWithUngroupMixin']
+__all__ = [
+ 'UserPermissionCacheMixin', 'GrantAssetsMixin', 'NodesWithUngroupMixin',
+ 'UserPermissionMixin',
+]
+
+
+class UserPermissionMixin:
+ permission_classes = (IsOrgAdminOrAppUser,)
+ obj = None
+
+ def initial(self, *args, **kwargs):
+ super().initial(*args, *kwargs)
+ self.obj = self.get_obj()
+
+ def get(self, request, *args, **kwargs):
+ set_to_root_org()
+ return super().get(request, *args, **kwargs)
+
+ def get_obj(self):
+ user_id = self.kwargs.get('pk', '')
+ if user_id:
+ user = get_object_or_404(User, id=user_id)
+ else:
+ user = self.request.user
+ return user
+
+ def get_permissions(self):
+ if self.kwargs.get('pk') is None:
+ self.permission_classes = (IsValidUser,)
+ return super().get_permissions()
# def get_etag(request, *args, **kwargs):
diff --git a/apps/perms/api/user_permission.py b/apps/perms/api/user_permission.py
index 7c7e513cf..0754e52c8 100644
--- a/apps/perms/api/user_permission.py
+++ b/apps/perms/api/user_permission.py
@@ -8,16 +8,16 @@ from rest_framework.generics import (
ListAPIView, get_object_or_404, RetrieveAPIView
)
-from common.permissions import IsValidUser, IsOrgAdminOrAppUser, IsOrgAdmin
+from common.permissions import IsOrgAdminOrAppUser, IsOrgAdmin
from common.tree import TreeNodeSerializer
from common.utils import get_logger
-from orgs.utils import set_to_root_org
from ..utils import (
ParserNode, AssetPermissionUtilV2
)
from ..hands import User, Asset, Node, SystemUser, NodeSerializer
from .. import serializers
from ..models import Action
+from .mixin import UserPermissionMixin
logger = get_logger(__name__)
@@ -39,32 +39,6 @@ __all__ = [
]
-class UserPermissionMixin:
- permission_classes = (IsOrgAdminOrAppUser,)
- obj = None
-
- def initial(self, *args, **kwargs):
- super().initial(*args, *kwargs)
- self.obj = self.get_obj()
-
- def get(self, request, *args, **kwargs):
- set_to_root_org()
- return super().get(request, *args, **kwargs)
-
- def get_obj(self):
- user_id = self.kwargs.get('pk', '')
- if user_id:
- user = get_object_or_404(User, id=user_id)
- else:
- user = self.request.user
- return user
-
- def get_permissions(self):
- if self.kwargs.get('pk') is None:
- self.permission_classes = (IsValidUser,)
- return super().get_permissions()
-
-
class UserAssetPermissionMixin(UserPermissionMixin):
util = None
diff --git a/apps/perms/api/user_remote_app_permission.py b/apps/perms/api/user_remote_app_permission.py
index e650022f6..84518c875 100644
--- a/apps/perms/api/user_remote_app_permission.py
+++ b/apps/perms/api/user_remote_app_permission.py
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
+import uuid
from django.shortcuts import get_object_or_404
from rest_framework.views import APIView, Response
from rest_framework.generics import (
@@ -12,13 +13,16 @@ from ..utils import (
RemoteAppPermissionUtil, construct_remote_apps_tree_root,
parse_remote_app_to_tree_node,
)
-from ..hands import User, RemoteAppSerializer, UserGroup
+from ..hands import User, RemoteApp, RemoteAppSerializer, UserGroup, SystemUser
from ..mixins import RemoteAppFilterMixin
+from .mixin import UserPermissionMixin
+from .. import serializers
__all__ = [
'UserGrantedRemoteAppsApi', 'ValidateUserRemoteAppPermissionApi',
'UserGrantedRemoteAppsAsTreeApi', 'UserGroupGrantedRemoteAppsApi',
+ 'UserGrantedRemoteAppSystemUsersApi',
]
@@ -65,18 +69,43 @@ class UserGrantedRemoteAppsAsTreeApi(UserGrantedRemoteAppsApi):
return super().get_serializer(data, many=True)
+class UserGrantedRemoteAppSystemUsersApi(UserPermissionMixin, ListAPIView):
+ permission_classes = (IsOrgAdminOrAppUser,)
+ serializer_class = serializers.RemoteAppSystemUserSerializer
+ only_fields = serializers.RemoteAppSystemUserSerializer.Meta.only_fields
+
+ def get_queryset(self):
+ util = RemoteAppPermissionUtil(self.obj)
+ remote_app_id = self.kwargs.get('remote_app_id')
+ remote_app = get_object_or_404(RemoteApp, id=remote_app_id)
+ system_users = util.get_remote_app_system_users(remote_app)
+ return system_users
+
+
class ValidateUserRemoteAppPermissionApi(APIView):
permission_classes = (IsOrgAdminOrAppUser,)
def get(self, request, *args, **kwargs):
user_id = request.query_params.get('user_id', '')
remote_app_id = request.query_params.get('remote_app_id', '')
+ system_id = request.query_params.get('system_user_id', '')
+
+ try:
+ user_id = uuid.UUID(user_id)
+ remote_app_id = uuid.UUID(remote_app_id)
+ system_id = uuid.UUID(system_id)
+ except ValueError:
+ return Response({'msg': False}, status=403)
user = get_object_or_404(User, id=user_id)
+ remote_app = get_object_or_404(RemoteApp, id=remote_app_id)
+ system_user = get_object_or_404(SystemUser, id=system_id)
+
util = RemoteAppPermissionUtil(user)
- remote_app = util.get_remote_apps().filter(id=remote_app_id).exists()
- if remote_app:
+ system_users = util.get_remote_app_system_users(remote_app)
+ if system_user in system_users:
return Response({'msg': True}, status=200)
+
return Response({'msg': False}, status=403)
diff --git a/apps/perms/forms/remote_app_permission.py b/apps/perms/forms/remote_app_permission.py
index 57fc3507a..d08066ba4 100644
--- a/apps/perms/forms/remote_app_permission.py
+++ b/apps/perms/forms/remote_app_permission.py
@@ -35,6 +35,9 @@ class RemoteAppPermissionCreateUpdateForm(OrgModelForm):
),
'remote_apps': forms.SelectMultiple(
attrs={'class': 'select2', 'data-placeholder': _('RemoteApp')}
+ ),
+ 'system_users': forms.SelectMultiple(
+ attrs={'class': 'select2', 'data-placeholder': _('System user')}
)
}
diff --git a/apps/perms/migrations/0008_remoteapppermission_system_users.py b/apps/perms/migrations/0008_remoteapppermission_system_users.py
new file mode 100644
index 000000000..26246929b
--- /dev/null
+++ b/apps/perms/migrations/0008_remoteapppermission_system_users.py
@@ -0,0 +1,32 @@
+# Generated by Django 2.1.7 on 2019-09-09 09:09
+
+from django.db import migrations, models
+from assets.models import SystemUser
+
+
+def migrate_system_user_from_remote_app_to_remote_app_perms(apps, schema_editor):
+ remote_app_perms_model = apps.get_model("perms", "RemoteAppPermission")
+ db_alias = schema_editor.connection.alias
+ perms = remote_app_perms_model.objects.using(db_alias).all()
+ for perm in perms:
+ system_users_ids = perm.remote_apps.values_list('system_user', flat=True)
+ perm.system_users.set(system_users_ids)
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('assets', '0037_auto_20190724_2002'),
+ ('perms', '0007_remove_assetpermission_actions'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='remoteapppermission',
+ name='system_users',
+ field=models.ManyToManyField(related_name='granted_by_remote_app_permissions', to='assets.SystemUser', verbose_name='System user'),
+ ),
+ migrations.RunPython(
+ code=migrate_system_user_from_remote_app_to_remote_app_perms,
+ ),
+ ]
diff --git a/apps/perms/models/remote_app_permission.py b/apps/perms/models/remote_app_permission.py
index 706467396..57a806b80 100644
--- a/apps/perms/models/remote_app_permission.py
+++ b/apps/perms/models/remote_app_permission.py
@@ -13,6 +13,7 @@ __all__ = [
class RemoteAppPermission(BasePermission):
remote_apps = models.ManyToManyField('applications.RemoteApp', related_name='granted_by_permissions', blank=True, verbose_name=_("RemoteApp"))
+ system_users = models.ManyToManyField('assets.SystemUser', related_name='granted_by_remote_app_permissions', verbose_name=_("System user"))
class Meta:
unique_together = [('org_id', 'name')]
diff --git a/apps/perms/serializers/remote_app_permission.py b/apps/perms/serializers/remote_app_permission.py
index 48a1fce0a..4361cff88 100644
--- a/apps/perms/serializers/remote_app_permission.py
+++ b/apps/perms/serializers/remote_app_permission.py
@@ -20,8 +20,8 @@ class RemoteAppPermissionSerializer(BulkOrgResourceModelSerializer):
model = RemoteAppPermission
list_serializer_class = AdaptedBulkListSerializer
fields = [
- 'id', 'name', 'users', 'user_groups', 'remote_apps', 'comment',
- 'is_active', 'date_start', 'date_expired', 'is_valid',
+ 'id', 'name', 'users', 'user_groups', 'remote_apps', 'system_users',
+ 'comment', 'is_active', 'date_start', 'date_expired', 'is_valid',
'created_by', 'date_created',
]
read_only_fields = ['created_by', 'date_created']
diff --git a/apps/perms/serializers/user_permission.py b/apps/perms/serializers/user_permission.py
index 149b618eb..d83c15b6f 100644
--- a/apps/perms/serializers/user_permission.py
+++ b/apps/perms/serializers/user_permission.py
@@ -12,6 +12,7 @@ __all__ = [
'NodeGrantedSerializer',
'AssetGrantedSerializer',
'ActionsSerializer', 'AssetSystemUserSerializer',
+ 'RemoteAppSystemUserSerializer',
]
@@ -24,13 +25,22 @@ class AssetSystemUserSerializer(serializers.ModelSerializer):
class Meta:
model = SystemUser
only_fields = (
- 'id', 'name', 'username', 'priority',
- 'protocol', 'login_mode',
+ 'id', 'name', 'username', 'priority', 'protocol', 'login_mode',
)
fields = list(only_fields) + ["actions"]
read_only_fields = fields
+class RemoteAppSystemUserSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = SystemUser
+ only_fields = (
+ 'id', 'name', 'username', 'priority', 'protocol', 'login_mode',
+ )
+ fields = list(only_fields)
+ read_only_fields = fields
+
+
class AssetGrantedSerializer(serializers.ModelSerializer):
"""
被授权资产的数据结构
diff --git a/apps/perms/templates/perms/remote_app_permission_create_update.html b/apps/perms/templates/perms/remote_app_permission_create_update.html
index 66bbcdffa..ce37d788b 100644
--- a/apps/perms/templates/perms/remote_app_permission_create_update.html
+++ b/apps/perms/templates/perms/remote_app_permission_create_update.html
@@ -47,6 +47,7 @@
{% trans 'RemoteApp' %}
{% bootstrap_field form.remote_apps layout="horizontal" %}
+ {% bootstrap_field form.system_users layout="horizontal" %}
{% trans 'Other' %}
@@ -127,7 +128,7 @@ $(document).ready(function () {
the_url = '{% url "api-perms:remote-app-permission-detail" pk=object.id %}';
method = "PUT";
{% endif %}
- objectAttrsIsList(data, ['users', 'user_groups', 'remote_apps']);
+ objectAttrsIsList(data, ['users', 'user_groups', 'remote_apps', 'system_users']);
objectAttrsIsDatetime(data, ['date_expired', 'date_start']);
objectAttrsIsBool(data, ['is_active']);
var props = {
diff --git a/apps/perms/templates/perms/remote_app_permission_detail.html b/apps/perms/templates/perms/remote_app_permission_detail.html
index d855e71a0..f2ec8fa35 100644
--- a/apps/perms/templates/perms/remote_app_permission_detail.html
+++ b/apps/perms/templates/perms/remote_app_permission_detail.html
@@ -126,7 +126,42 @@
+
+
+ {% trans 'System user' %}
+
+
+
+
+
+ {% for system_user in object.system_users.all %}
+
+ {{ system_user }} |
+
+
+ |
+
+ {% endfor %}
+
+
+
+
@@ -136,6 +171,20 @@
{% endblock %}
{% block custom_foot_js %}