mirror of https://github.com/jumpserver/jumpserver
perf: 干掉 applications
parent
05e2f8aaf6
commit
3011b18eaa
|
@ -1,3 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
# Register your models here.
|
|
|
@ -1,3 +0,0 @@
|
||||||
from .application import *
|
|
||||||
from .account import *
|
|
||||||
from .remote_app import *
|
|
|
@ -1,66 +0,0 @@
|
||||||
# coding: utf-8
|
|
||||||
#
|
|
||||||
|
|
||||||
from django_filters import rest_framework as filters
|
|
||||||
from django.db.models import Q
|
|
||||||
|
|
||||||
from common.drf.filters import BaseFilterSet
|
|
||||||
from common.drf.api import JMSBulkModelViewSet
|
|
||||||
from common.mixins import RecordViewLogMixin
|
|
||||||
from common.permissions import UserConfirmation
|
|
||||||
from authentication.const import ConfirmType
|
|
||||||
from rbac.permissions import RBACPermission
|
|
||||||
from assets.models import SystemUser
|
|
||||||
from ..models import Account
|
|
||||||
from .. import serializers
|
|
||||||
|
|
||||||
|
|
||||||
class AccountFilterSet(BaseFilterSet):
|
|
||||||
username = filters.CharFilter(method='do_nothing')
|
|
||||||
type = filters.CharFilter(field_name='type', lookup_expr='exact')
|
|
||||||
category = filters.CharFilter(field_name='category', lookup_expr='exact')
|
|
||||||
app_display = filters.CharFilter(field_name='app_display', lookup_expr='exact')
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Account
|
|
||||||
fields = ['app', 'systemuser']
|
|
||||||
|
|
||||||
@property
|
|
||||||
def qs(self):
|
|
||||||
qs = super().qs
|
|
||||||
qs = self.filter_username(qs)
|
|
||||||
return qs
|
|
||||||
|
|
||||||
def filter_username(self, qs):
|
|
||||||
username = self.get_query_param('username')
|
|
||||||
if not username:
|
|
||||||
return qs
|
|
||||||
q = Q(username=username) | Q(systemuser__username=username)
|
|
||||||
qs = qs.filter(q).distinct()
|
|
||||||
return qs
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationAccountViewSet(JMSBulkModelViewSet):
|
|
||||||
model = Account
|
|
||||||
search_fields = ['username', 'app_display']
|
|
||||||
filterset_class = AccountFilterSet
|
|
||||||
filterset_fields = ['username', 'app_display', 'type', 'category', 'app']
|
|
||||||
serializer_class = serializers.AppAccountSerializer
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
queryset = Account.get_queryset()
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
|
|
||||||
class SystemUserAppRelationViewSet(ApplicationAccountViewSet):
|
|
||||||
perm_model = SystemUser
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationAccountSecretViewSet(RecordViewLogMixin, ApplicationAccountViewSet):
|
|
||||||
serializer_class = serializers.AppAccountSecretSerializer
|
|
||||||
permission_classes = [RBACPermission, UserConfirmation.require(ConfirmType.MFA)]
|
|
||||||
http_method_names = ['get', 'options']
|
|
||||||
rbac_perms = {
|
|
||||||
'retrieve': 'applications.view_applicationaccountsecret',
|
|
||||||
'list': 'applications.view_applicationaccountsecret',
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
# coding: utf-8
|
|
||||||
#
|
|
||||||
from orgs.mixins.api import OrgBulkModelViewSet
|
|
||||||
from rest_framework.decorators import action
|
|
||||||
from rest_framework.response import Response
|
|
||||||
|
|
||||||
from common.tree import TreeNodeSerializer
|
|
||||||
from common.mixins.api import SuggestionMixin
|
|
||||||
from .. import serializers
|
|
||||||
from ..models import Application
|
|
||||||
|
|
||||||
__all__ = ['ApplicationViewSet']
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationViewSet(SuggestionMixin, OrgBulkModelViewSet):
|
|
||||||
model = Application
|
|
||||||
filterset_fields = {
|
|
||||||
'name': ['exact'],
|
|
||||||
'category': ['exact', 'in'],
|
|
||||||
'type': ['exact', 'in'],
|
|
||||||
}
|
|
||||||
search_fields = ('name', 'type', 'category')
|
|
||||||
serializer_classes = {
|
|
||||||
'default': serializers.AppSerializer,
|
|
||||||
'get_tree': TreeNodeSerializer,
|
|
||||||
'suggestion': serializers.MiniAppSerializer
|
|
||||||
}
|
|
||||||
rbac_perms = {
|
|
||||||
'get_tree': 'applications.view_application',
|
|
||||||
'match': 'applications.match_application'
|
|
||||||
}
|
|
||||||
|
|
||||||
@action(methods=['GET'], detail=False, url_path='tree')
|
|
||||||
def get_tree(self, request, *args, **kwargs):
|
|
||||||
show_count = request.query_params.get('show_count', '1') == '1'
|
|
||||||
queryset = self.filter_queryset(self.get_queryset())
|
|
||||||
tree_nodes = Application.create_tree_nodes(queryset, show_count=show_count)
|
|
||||||
serializer = self.get_serializer(tree_nodes, many=True)
|
|
||||||
return Response(serializer.data)
|
|
|
@ -1,16 +0,0 @@
|
||||||
# coding: utf-8
|
|
||||||
#
|
|
||||||
|
|
||||||
from orgs.mixins import generics
|
|
||||||
from .. import models
|
|
||||||
from ..serializers import RemoteAppConnectionInfoSerializer
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'RemoteAppConnectionInfoApi',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class RemoteAppConnectionInfoApi(generics.RetrieveAPIView):
|
|
||||||
model = models.Application
|
|
||||||
serializer_class = RemoteAppConnectionInfoSerializer
|
|
|
@ -1,14 +0,0 @@
|
||||||
"""
|
|
||||||
jumpserver.__app__.hands.py
|
|
||||||
~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
This app depends other apps api, function .. should be import or write mack here.
|
|
||||||
|
|
||||||
Other module of this app shouldn't connect with other app.
|
|
||||||
|
|
||||||
:copyright: (c) 2014-2018 by JumpServer Team.
|
|
||||||
:license: GPL v2, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
from users.models import User, UserGroup
|
|
|
@ -5,7 +5,6 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from common.tree import TreeNode
|
from common.tree import TreeNode
|
||||||
from ..utils import KubernetesTree
|
|
||||||
from .. import const
|
from .. import const
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
from rest_framework import permissions
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['IsRemoteApp']
|
|
||||||
|
|
||||||
|
|
||||||
class IsRemoteApp(permissions.BasePermission):
|
|
||||||
def has_object_permission(self, request, view, obj):
|
|
||||||
return obj.category_remote_app
|
|
|
@ -1,2 +0,0 @@
|
||||||
from .application import *
|
|
||||||
from .remote_app import *
|
|
|
@ -1,168 +0,0 @@
|
||||||
# coding: utf-8
|
|
||||||
#
|
|
||||||
from rest_framework import serializers
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
|
||||||
from assets.serializers.base import AuthSerializerMixin
|
|
||||||
from common.drf.serializers import MethodSerializer, SecretReadableMixin
|
|
||||||
from .attrs import (
|
|
||||||
category_serializer_classes_mapping,
|
|
||||||
type_serializer_classes_mapping,
|
|
||||||
type_secret_serializer_classes_mapping
|
|
||||||
)
|
|
||||||
from .. import models
|
|
||||||
from .. import const
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'AppSerializer', 'MiniAppSerializer', 'AppSerializerMixin',
|
|
||||||
'AppAccountSerializer', 'AppAccountSecretSerializer'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class AppSerializerMixin(serializers.Serializer):
|
|
||||||
attrs = MethodSerializer()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def app(self):
|
|
||||||
if isinstance(self.instance, models.Application):
|
|
||||||
instance = self.instance
|
|
||||||
else:
|
|
||||||
instance = None
|
|
||||||
return instance
|
|
||||||
|
|
||||||
def get_attrs_serializer(self):
|
|
||||||
default_serializer = serializers.Serializer(read_only=True)
|
|
||||||
instance = self.app
|
|
||||||
if instance:
|
|
||||||
_type = instance.type
|
|
||||||
_category = instance.category
|
|
||||||
else:
|
|
||||||
_type = self.context['request'].query_params.get('type')
|
|
||||||
_category = self.context['request'].query_params.get('category')
|
|
||||||
if _type:
|
|
||||||
if isinstance(self, AppAccountSecretSerializer):
|
|
||||||
serializer_class = type_secret_serializer_classes_mapping.get(_type)
|
|
||||||
else:
|
|
||||||
serializer_class = type_serializer_classes_mapping.get(_type)
|
|
||||||
elif _category:
|
|
||||||
serializer_class = category_serializer_classes_mapping.get(_category)
|
|
||||||
else:
|
|
||||||
serializer_class = default_serializer
|
|
||||||
|
|
||||||
if not serializer_class:
|
|
||||||
serializer_class = default_serializer
|
|
||||||
|
|
||||||
if isinstance(serializer_class, type):
|
|
||||||
serializer = serializer_class()
|
|
||||||
else:
|
|
||||||
serializer = serializer_class
|
|
||||||
return serializer
|
|
||||||
|
|
||||||
def create(self, validated_data):
|
|
||||||
return super().create(validated_data)
|
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
|
||||||
return super().update(instance, validated_data)
|
|
||||||
|
|
||||||
|
|
||||||
class AppSerializer(AppSerializerMixin, BulkOrgResourceModelSerializer):
|
|
||||||
category_display = serializers.ReadOnlyField(source='get_category_display', label=_('Category display'))
|
|
||||||
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display'))
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = models.Application
|
|
||||||
fields_mini = ['id', 'name']
|
|
||||||
fields_small = fields_mini + [
|
|
||||||
'category', 'category_display', 'type', 'type_display',
|
|
||||||
'attrs', 'date_created', 'date_updated', 'created_by', 'comment'
|
|
||||||
]
|
|
||||||
fields_fk = ['domain']
|
|
||||||
fields = fields_small + fields_fk
|
|
||||||
read_only_fields = [
|
|
||||||
'created_by', 'date_created', 'date_updated', 'get_type_display',
|
|
||||||
]
|
|
||||||
|
|
||||||
def validate_attrs(self, attrs):
|
|
||||||
_attrs = self.instance.attrs if self.instance else {}
|
|
||||||
_attrs.update(attrs)
|
|
||||||
return _attrs
|
|
||||||
|
|
||||||
|
|
||||||
class MiniAppSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = models.Application
|
|
||||||
fields = AppSerializer.Meta.fields_mini
|
|
||||||
|
|
||||||
|
|
||||||
class AppAccountSerializer(AppSerializerMixin, AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
|
||||||
category = serializers.ChoiceField(label=_('Category'), choices=const.AppCategory.choices, read_only=True)
|
|
||||||
category_display = serializers.SerializerMethodField(label=_('Category display'))
|
|
||||||
type = serializers.ChoiceField(label=_('Type'), choices=const.AppType.choices, read_only=True)
|
|
||||||
type_display = serializers.SerializerMethodField(label=_('Type display'))
|
|
||||||
date_created = serializers.DateTimeField(label=_('Date created'), format="%Y/%m/%d %H:%M:%S", read_only=True)
|
|
||||||
date_updated = serializers.DateTimeField(label=_('Date updated'), format="%Y/%m/%d %H:%M:%S", read_only=True)
|
|
||||||
|
|
||||||
category_mapper = dict(const.AppCategory.choices)
|
|
||||||
type_mapper = dict(const.AppType.choices)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = models.Account
|
|
||||||
fields_mini = ['id', 'username', 'version']
|
|
||||||
fields_write_only = ['password', 'private_key', 'public_key', 'passphrase']
|
|
||||||
fields_other = ['date_created', 'date_updated']
|
|
||||||
fields_fk = ['systemuser', 'systemuser_display', 'app', 'app_display']
|
|
||||||
fields = fields_mini + fields_fk + fields_write_only + fields_other + [
|
|
||||||
'type', 'type_display', 'category', 'category_display', 'attrs'
|
|
||||||
]
|
|
||||||
extra_kwargs = {
|
|
||||||
'username': {'default': '', 'required': False},
|
|
||||||
'password': {'write_only': True},
|
|
||||||
'app_display': {'label': _('Application display')},
|
|
||||||
'systemuser_display': {'label': _('System User')},
|
|
||||||
'account': {'label': _('account')}
|
|
||||||
}
|
|
||||||
use_model_bulk_create = True
|
|
||||||
model_bulk_create_kwargs = {
|
|
||||||
'ignore_conflicts': True
|
|
||||||
}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def app(self):
|
|
||||||
if isinstance(self.instance, models.Account):
|
|
||||||
instance = self.instance.app
|
|
||||||
else:
|
|
||||||
instance = None
|
|
||||||
return instance
|
|
||||||
|
|
||||||
def get_category_display(self, obj):
|
|
||||||
return self.category_mapper.get(obj.category)
|
|
||||||
|
|
||||||
def get_type_display(self, obj):
|
|
||||||
return self.type_mapper.get(obj.type)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def setup_eager_loading(cls, queryset):
|
|
||||||
""" Perform necessary eager loading of data. """
|
|
||||||
queryset = queryset.prefetch_related('systemuser', 'app')
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
def to_representation(self, instance):
|
|
||||||
instance.load_auth()
|
|
||||||
return super().to_representation(instance)
|
|
||||||
|
|
||||||
|
|
||||||
class AppAccountSecretSerializer(SecretReadableMixin, AppAccountSerializer):
|
|
||||||
class Meta(AppAccountSerializer.Meta):
|
|
||||||
fields_backup = [
|
|
||||||
'id', 'app_display', 'attrs', 'username', 'password', 'private_key',
|
|
||||||
'public_key', 'date_created', 'date_updated', 'version'
|
|
||||||
]
|
|
||||||
|
|
||||||
extra_kwargs = {
|
|
||||||
'password': {'write_only': False},
|
|
||||||
'private_key': {'write_only': False},
|
|
||||||
'public_key': {'write_only': False},
|
|
||||||
'app_display': {'label': _('Application display')},
|
|
||||||
'systemuser_display': {'label': _('System User')}
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
from .attrs import *
|
|
|
@ -1,3 +0,0 @@
|
||||||
from .remote_app import *
|
|
||||||
from .db import *
|
|
||||||
from .cloud import *
|
|
|
@ -1,8 +0,0 @@
|
||||||
from rest_framework import serializers
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
__all__ = ['CloudSerializer']
|
|
||||||
|
|
||||||
|
|
||||||
class CloudSerializer(serializers.Serializer):
|
|
||||||
cluster = serializers.CharField(max_length=1024, label=_('Cluster'), allow_null=True)
|
|
|
@ -1,15 +0,0 @@
|
||||||
# coding: utf-8
|
|
||||||
#
|
|
||||||
from rest_framework import serializers
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['DBSerializer']
|
|
||||||
|
|
||||||
|
|
||||||
class DBSerializer(serializers.Serializer):
|
|
||||||
host = serializers.CharField(max_length=128, label=_('Host'), allow_null=True)
|
|
||||||
port = serializers.IntegerField(label=_('Port'), allow_null=True)
|
|
||||||
database = serializers.CharField(
|
|
||||||
max_length=128, required=True, allow_null=True, label=_('Database')
|
|
||||||
)
|
|
|
@ -1,60 +0,0 @@
|
||||||
# coding: utf-8
|
|
||||||
#
|
|
||||||
|
|
||||||
from rest_framework import serializers
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
|
||||||
|
|
||||||
from common.utils import get_logger, is_uuid, get_object_or_none
|
|
||||||
from assets.models import Asset
|
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
|
||||||
|
|
||||||
__all__ = ['RemoteAppSerializer']
|
|
||||||
|
|
||||||
|
|
||||||
class ExistAssetPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
|
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
|
||||||
instance = super().to_internal_value(data)
|
|
||||||
return str(instance.id)
|
|
||||||
|
|
||||||
def to_representation(self, _id):
|
|
||||||
# _id 是 instance.id
|
|
||||||
if self.pk_field is not None:
|
|
||||||
return self.pk_field.to_representation(_id)
|
|
||||||
# 解决删除资产后,远程应用更新页面会显示资产ID的问题
|
|
||||||
asset = get_object_or_none(Asset, id=_id)
|
|
||||||
if not asset:
|
|
||||||
return None
|
|
||||||
return _id
|
|
||||||
|
|
||||||
|
|
||||||
class RemoteAppSerializer(serializers.Serializer):
|
|
||||||
asset_info = serializers.SerializerMethodField()
|
|
||||||
asset = ExistAssetPrimaryKeyRelatedField(
|
|
||||||
queryset=Asset.objects, required=True, label=_("Asset"), allow_null=True
|
|
||||||
)
|
|
||||||
path = serializers.CharField(
|
|
||||||
max_length=128, label=_('Application path'), allow_null=True
|
|
||||||
)
|
|
||||||
|
|
||||||
def validate_asset(self, asset):
|
|
||||||
if not asset:
|
|
||||||
raise serializers.ValidationError(_('This field is required.'))
|
|
||||||
return asset
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_asset_info(obj):
|
|
||||||
asset_id = obj.get('asset')
|
|
||||||
if not asset_id or not is_uuid(asset_id):
|
|
||||||
return {}
|
|
||||||
try:
|
|
||||||
asset = Asset.objects.get(id=str(asset_id))
|
|
||||||
except ObjectDoesNotExist as e:
|
|
||||||
logger.error(e)
|
|
||||||
return {}
|
|
||||||
if not asset:
|
|
||||||
return {}
|
|
||||||
asset_info = {'id': str(asset.id), 'hostname': asset.hostname}
|
|
||||||
return asset_info
|
|
|
@ -1,15 +0,0 @@
|
||||||
|
|
||||||
from .mysql import *
|
|
||||||
from .mariadb import *
|
|
||||||
from .oracle import *
|
|
||||||
from .pgsql import *
|
|
||||||
from .sqlserver import *
|
|
||||||
from .redis import *
|
|
||||||
from .mongodb import *
|
|
||||||
|
|
||||||
from .chrome import *
|
|
||||||
from .mysql_workbench import *
|
|
||||||
from .vmware_client import *
|
|
||||||
from .custom import *
|
|
||||||
|
|
||||||
from .k8s import *
|
|
|
@ -1,34 +0,0 @@
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
from rest_framework import serializers
|
|
||||||
|
|
||||||
from common.drf.fields import EncryptedField
|
|
||||||
from ..application_category import RemoteAppSerializer
|
|
||||||
|
|
||||||
__all__ = ['ChromeSerializer', 'ChromeSecretSerializer']
|
|
||||||
|
|
||||||
|
|
||||||
class ChromeSerializer(RemoteAppSerializer):
|
|
||||||
CHROME_PATH = 'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe'
|
|
||||||
|
|
||||||
path = serializers.CharField(
|
|
||||||
max_length=128, label=_('Application path'), default=CHROME_PATH, allow_null=True,
|
|
||||||
)
|
|
||||||
chrome_target = serializers.CharField(
|
|
||||||
max_length=128, allow_blank=True, required=False,
|
|
||||||
label=_('Target URL'), allow_null=True,
|
|
||||||
)
|
|
||||||
chrome_username = serializers.CharField(
|
|
||||||
max_length=128, allow_blank=True, required=False,
|
|
||||||
label=_('Chrome username'), allow_null=True,
|
|
||||||
)
|
|
||||||
chrome_password = EncryptedField(
|
|
||||||
max_length=128, allow_blank=True, required=False,
|
|
||||||
label=_('Chrome password'), allow_null=True
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ChromeSecretSerializer(ChromeSerializer):
|
|
||||||
chrome_password = EncryptedField(
|
|
||||||
max_length=128, allow_blank=True, required=False,
|
|
||||||
label=_('Chrome password'), allow_null=True, write_only=False
|
|
||||||
)
|
|
|
@ -1,33 +0,0 @@
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
from rest_framework import serializers
|
|
||||||
|
|
||||||
from common.drf.fields import EncryptedField
|
|
||||||
from ..application_category import RemoteAppSerializer
|
|
||||||
|
|
||||||
__all__ = ['CustomSerializer', 'CustomSecretSerializer']
|
|
||||||
|
|
||||||
|
|
||||||
class CustomSerializer(RemoteAppSerializer):
|
|
||||||
custom_cmdline = serializers.CharField(
|
|
||||||
max_length=128, allow_blank=True, required=False, label=_('Operating parameter'),
|
|
||||||
allow_null=True,
|
|
||||||
)
|
|
||||||
custom_target = serializers.CharField(
|
|
||||||
max_length=128, allow_blank=True, required=False, label=_('Target url'),
|
|
||||||
allow_null=True,
|
|
||||||
)
|
|
||||||
custom_username = serializers.CharField(
|
|
||||||
max_length=128, allow_blank=True, required=False, label=_('Custom Username'),
|
|
||||||
allow_null=True,
|
|
||||||
)
|
|
||||||
custom_password = EncryptedField(
|
|
||||||
max_length=128, allow_blank=True, required=False,
|
|
||||||
label=_('Custom password'), allow_null=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CustomSecretSerializer(RemoteAppSerializer):
|
|
||||||
custom_password = EncryptedField(
|
|
||||||
max_length=128, allow_blank=True, required=False, write_only=False,
|
|
||||||
label=_('Custom password'), allow_null=True,
|
|
||||||
)
|
|
|
@ -1,7 +0,0 @@
|
||||||
from ..application_category import CloudSerializer
|
|
||||||
|
|
||||||
__all__ = ['K8SSerializer']
|
|
||||||
|
|
||||||
|
|
||||||
class K8SSerializer(CloudSerializer):
|
|
||||||
pass
|
|
|
@ -1,7 +0,0 @@
|
||||||
from .mysql import MySQLSerializer
|
|
||||||
|
|
||||||
__all__ = ['MariaDBSerializer']
|
|
||||||
|
|
||||||
|
|
||||||
class MariaDBSerializer(MySQLSerializer):
|
|
||||||
pass
|
|
|
@ -1,11 +0,0 @@
|
||||||
from rest_framework import serializers
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
from ..application_category import DBSerializer
|
|
||||||
|
|
||||||
__all__ = ['MongoDBSerializer']
|
|
||||||
|
|
||||||
|
|
||||||
class MongoDBSerializer(DBSerializer):
|
|
||||||
port = serializers.IntegerField(default=27017, label=_('Port'), allow_null=True)
|
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
from rest_framework import serializers
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
from ..application_category import DBSerializer
|
|
||||||
|
|
||||||
__all__ = ['MySQLSerializer']
|
|
||||||
|
|
||||||
|
|
||||||
class MySQLSerializer(DBSerializer):
|
|
||||||
port = serializers.IntegerField(default=3306, label=_('Port'), allow_null=True)
|
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
from rest_framework import serializers
|
|
||||||
|
|
||||||
from common.drf.fields import EncryptedField
|
|
||||||
from ..application_category import RemoteAppSerializer
|
|
||||||
|
|
||||||
__all__ = ['MySQLWorkbenchSerializer', 'MySQLWorkbenchSecretSerializer']
|
|
||||||
|
|
||||||
|
|
||||||
class MySQLWorkbenchSerializer(RemoteAppSerializer):
|
|
||||||
MYSQL_WORKBENCH_PATH = 'C:\Program Files\MySQL\MySQL Workbench 8.0 CE\MySQLWorkbench.exe'
|
|
||||||
|
|
||||||
path = serializers.CharField(
|
|
||||||
max_length=128, label=_('Application path'), default=MYSQL_WORKBENCH_PATH,
|
|
||||||
allow_null=True,
|
|
||||||
)
|
|
||||||
mysql_workbench_ip = serializers.CharField(
|
|
||||||
max_length=128, allow_blank=True, required=False, label=_('IP'),
|
|
||||||
allow_null=True,
|
|
||||||
)
|
|
||||||
mysql_workbench_port = serializers.IntegerField(
|
|
||||||
required=False, label=_('Port'),
|
|
||||||
allow_null=True,
|
|
||||||
)
|
|
||||||
mysql_workbench_name = serializers.CharField(
|
|
||||||
max_length=128, allow_blank=True, required=False, label=_('Database'),
|
|
||||||
allow_null=True,
|
|
||||||
)
|
|
||||||
mysql_workbench_username = serializers.CharField(
|
|
||||||
max_length=128, allow_blank=True, required=False, label=_('Mysql workbench username'),
|
|
||||||
allow_null=True,
|
|
||||||
)
|
|
||||||
mysql_workbench_password = EncryptedField(
|
|
||||||
max_length=128, allow_blank=True, required=False,
|
|
||||||
label=_('Mysql workbench password'), allow_null=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MySQLWorkbenchSecretSerializer(RemoteAppSerializer):
|
|
||||||
mysql_workbench_password = EncryptedField(
|
|
||||||
max_length=128, allow_blank=True, required=False, write_only=False,
|
|
||||||
label=_('Mysql workbench password'), allow_null=True,
|
|
||||||
)
|
|
|
@ -1,10 +0,0 @@
|
||||||
from rest_framework import serializers
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
from ..application_category import DBSerializer
|
|
||||||
|
|
||||||
__all__ = ['OracleSerializer']
|
|
||||||
|
|
||||||
|
|
||||||
class OracleSerializer(DBSerializer):
|
|
||||||
port = serializers.IntegerField(default=1521, label=_('Port'), allow_null=True)
|
|
|
@ -1,10 +0,0 @@
|
||||||
from rest_framework import serializers
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
from ..application_category import DBSerializer
|
|
||||||
|
|
||||||
__all__ = ['PostgreSerializer']
|
|
||||||
|
|
||||||
|
|
||||||
class PostgreSerializer(DBSerializer):
|
|
||||||
port = serializers.IntegerField(default=5432, label=_('Port'), allow_null=True)
|
|
|
@ -1,11 +0,0 @@
|
||||||
from rest_framework import serializers
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
from ..application_category import DBSerializer
|
|
||||||
|
|
||||||
__all__ = ['RedisSerializer']
|
|
||||||
|
|
||||||
|
|
||||||
class RedisSerializer(DBSerializer):
|
|
||||||
port = serializers.IntegerField(default=6379, label=_('Port'), allow_null=True)
|
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
from rest_framework import serializers
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
from ..application_category import DBSerializer
|
|
||||||
|
|
||||||
__all__ = ['SQLServerSerializer']
|
|
||||||
|
|
||||||
|
|
||||||
class SQLServerSerializer(DBSerializer):
|
|
||||||
port = serializers.IntegerField(default=1433, label=_('Port'), allow_null=True)
|
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
from rest_framework import serializers
|
|
||||||
|
|
||||||
from common.drf.fields import EncryptedField
|
|
||||||
from ..application_category import RemoteAppSerializer
|
|
||||||
|
|
||||||
__all__ = ['VMwareClientSerializer', 'VMwareClientSecretSerializer']
|
|
||||||
|
|
||||||
|
|
||||||
class VMwareClientSerializer(RemoteAppSerializer):
|
|
||||||
PATH = r'''
|
|
||||||
C:\Program Files (x86)\VMware\Infrastructure\Virtual Infrastructure Client\Launcher\VpxClient
|
|
||||||
.exe
|
|
||||||
'''
|
|
||||||
VMWARE_CLIENT_PATH = ''.join(PATH.split())
|
|
||||||
|
|
||||||
path = serializers.CharField(
|
|
||||||
max_length=128, label=_('Application path'), default=VMWARE_CLIENT_PATH,
|
|
||||||
allow_null=True
|
|
||||||
)
|
|
||||||
vmware_target = serializers.CharField(
|
|
||||||
max_length=128, allow_blank=True, required=False, label=_('Target URL'),
|
|
||||||
allow_null=True
|
|
||||||
)
|
|
||||||
vmware_username = serializers.CharField(
|
|
||||||
max_length=128, allow_blank=True, required=False, label=_('Vmware username'),
|
|
||||||
allow_null=True
|
|
||||||
)
|
|
||||||
vmware_password = EncryptedField(
|
|
||||||
max_length=128, allow_blank=True, required=False,
|
|
||||||
label=_('Vmware password'), allow_null=True
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class VMwareClientSecretSerializer(RemoteAppSerializer):
|
|
||||||
vmware_password = EncryptedField(
|
|
||||||
max_length=128, allow_blank=True, required=False, write_only=False,
|
|
||||||
label=_('Vmware password'), allow_null=True
|
|
||||||
)
|
|
|
@ -1,62 +0,0 @@
|
||||||
import copy
|
|
||||||
|
|
||||||
from applications import const
|
|
||||||
from . import application_category, application_type
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'category_serializer_classes_mapping',
|
|
||||||
'type_serializer_classes_mapping',
|
|
||||||
'get_serializer_class_by_application_type',
|
|
||||||
'type_secret_serializer_classes_mapping'
|
|
||||||
]
|
|
||||||
|
|
||||||
# define `attrs` field `category serializers mapping`
|
|
||||||
# ---------------------------------------------------
|
|
||||||
|
|
||||||
category_serializer_classes_mapping = {
|
|
||||||
const.AppCategory.db.value: application_category.DBSerializer,
|
|
||||||
const.AppCategory.remote_app.value: application_category.RemoteAppSerializer,
|
|
||||||
const.AppCategory.cloud.value: application_category.CloudSerializer,
|
|
||||||
}
|
|
||||||
|
|
||||||
# define `attrs` field `type serializers mapping`
|
|
||||||
# -----------------------------------------------
|
|
||||||
|
|
||||||
type_serializer_classes_mapping = {
|
|
||||||
# db
|
|
||||||
const.AppType.mysql.value: application_type.MySQLSerializer,
|
|
||||||
const.AppType.mariadb.value: application_type.MariaDBSerializer,
|
|
||||||
const.AppType.oracle.value: application_type.OracleSerializer,
|
|
||||||
const.AppType.pgsql.value: application_type.PostgreSerializer,
|
|
||||||
const.AppType.sqlserver.value: application_type.SQLServerSerializer,
|
|
||||||
const.AppType.redis.value: application_type.RedisSerializer,
|
|
||||||
const.AppType.mongodb.value: application_type.MongoDBSerializer,
|
|
||||||
# cloud
|
|
||||||
const.AppType.k8s.value: application_type.K8SSerializer
|
|
||||||
}
|
|
||||||
|
|
||||||
remote_app_serializer_classes_mapping = {
|
|
||||||
# remote-app
|
|
||||||
const.AppType.chrome.value: application_type.ChromeSerializer,
|
|
||||||
const.AppType.mysql_workbench.value: application_type.MySQLWorkbenchSerializer,
|
|
||||||
const.AppType.vmware_client.value: application_type.VMwareClientSerializer,
|
|
||||||
const.AppType.custom.value: application_type.CustomSerializer
|
|
||||||
}
|
|
||||||
|
|
||||||
type_serializer_classes_mapping.update(remote_app_serializer_classes_mapping)
|
|
||||||
|
|
||||||
remote_app_secret_serializer_classes_mapping = {
|
|
||||||
# remote-app
|
|
||||||
const.AppType.chrome.value: application_type.ChromeSecretSerializer,
|
|
||||||
const.AppType.mysql_workbench.value: application_type.MySQLWorkbenchSecretSerializer,
|
|
||||||
const.AppType.vmware_client.value: application_type.VMwareClientSecretSerializer,
|
|
||||||
const.AppType.custom.value: application_type.CustomSecretSerializer
|
|
||||||
}
|
|
||||||
|
|
||||||
type_secret_serializer_classes_mapping = copy.deepcopy(type_serializer_classes_mapping)
|
|
||||||
|
|
||||||
type_secret_serializer_classes_mapping.update(remote_app_secret_serializer_classes_mapping)
|
|
||||||
|
|
||||||
|
|
||||||
def get_serializer_class_by_application_type(_application_type):
|
|
||||||
return type_serializer_classes_mapping.get(_application_type)
|
|
|
@ -1,31 +0,0 @@
|
||||||
# coding: utf-8
|
|
||||||
#
|
|
||||||
from rest_framework import serializers
|
|
||||||
from common.utils import get_logger
|
|
||||||
from ..models import Application
|
|
||||||
|
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['RemoteAppConnectionInfoSerializer']
|
|
||||||
|
|
||||||
|
|
||||||
class RemoteAppConnectionInfoSerializer(serializers.ModelSerializer):
|
|
||||||
parameter_remote_app = serializers.SerializerMethodField()
|
|
||||||
asset = serializers.SerializerMethodField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Application
|
|
||||||
fields = [
|
|
||||||
'id', 'name', 'asset', 'parameter_remote_app',
|
|
||||||
]
|
|
||||||
read_only_fields = ['parameter_remote_app']
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_asset(obj):
|
|
||||||
return obj.attrs.get('asset')
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_parameter_remote_app(obj):
|
|
||||||
return obj.get_rdp_remote_app_setting()
|
|
|
@ -1,3 +0,0 @@
|
||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
|
@ -1,7 +0,0 @@
|
||||||
# coding: utf-8
|
|
||||||
#
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
|
|
||||||
]
|
|
|
@ -1,25 +0,0 @@
|
||||||
# coding:utf-8
|
|
||||||
#
|
|
||||||
from django.urls import path
|
|
||||||
from rest_framework_bulk.routes import BulkRouter
|
|
||||||
from .. import api
|
|
||||||
|
|
||||||
|
|
||||||
app_name = 'applications'
|
|
||||||
|
|
||||||
|
|
||||||
router = BulkRouter()
|
|
||||||
router.register(r'applications', api.ApplicationViewSet, 'application')
|
|
||||||
router.register(r'accounts', api.ApplicationAccountViewSet, 'application-account')
|
|
||||||
router.register(r'system-users-apps-relations', api.SystemUserAppRelationViewSet, 'system-users-apps-relation')
|
|
||||||
router.register(r'account-secrets', api.ApplicationAccountSecretViewSet, 'application-account-secret')
|
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path('remote-apps/<uuid:pk>/connection-info/', api.RemoteAppConnectionInfoApi.as_view(), name='remote-app-connection-info'),
|
|
||||||
# path('accounts/', api.ApplicationAccountViewSet.as_view(), name='application-account'),
|
|
||||||
# path('account-secrets/', api.ApplicationAccountSecretViewSet.as_view(), name='application-account-secret')
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
urlpatterns += router.urls
|
|
|
@ -1,4 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
|
|
||||||
from .kubernetes_util import *
|
|
|
@ -1,186 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from urllib3.exceptions import MaxRetryError
|
|
||||||
from urllib.parse import urlencode
|
|
||||||
|
|
||||||
from kubernetes.client import api_client
|
|
||||||
from kubernetes.client.api import core_v1_api
|
|
||||||
from kubernetes import client
|
|
||||||
from kubernetes.client.exceptions import ApiException
|
|
||||||
|
|
||||||
from rest_framework.generics import get_object_or_404
|
|
||||||
|
|
||||||
from common.utils import get_logger
|
|
||||||
from common.tree import TreeNode
|
|
||||||
from assets.models import SystemUser
|
|
||||||
|
|
||||||
from .. import const
|
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
|
||||||
|
|
||||||
|
|
||||||
class KubernetesClient:
|
|
||||||
def __init__(self, url, token):
|
|
||||||
self.url = url
|
|
||||||
self.token = token
|
|
||||||
|
|
||||||
def get_api(self):
|
|
||||||
configuration = client.Configuration()
|
|
||||||
configuration.host = self.url
|
|
||||||
configuration.verify_ssl = False
|
|
||||||
configuration.api_key = {"authorization": "Bearer " + self.token}
|
|
||||||
c = api_client.ApiClient(configuration=configuration)
|
|
||||||
api = core_v1_api.CoreV1Api(c)
|
|
||||||
return api
|
|
||||||
|
|
||||||
def get_namespace_list(self):
|
|
||||||
api = self.get_api()
|
|
||||||
namespace_list = []
|
|
||||||
for ns in api.list_namespace().items:
|
|
||||||
namespace_list.append(ns.metadata.name)
|
|
||||||
return namespace_list
|
|
||||||
|
|
||||||
def get_services(self):
|
|
||||||
api = self.get_api()
|
|
||||||
ret = api.list_service_for_all_namespaces(watch=False)
|
|
||||||
for i in ret.items:
|
|
||||||
print("%s \t%s \t%s \t%s \t%s \n" % (
|
|
||||||
i.kind, i.metadata.namespace, i.metadata.name, i.spec.cluster_ip, i.spec.ports))
|
|
||||||
|
|
||||||
def get_pod_info(self, namespace, pod):
|
|
||||||
api = self.get_api()
|
|
||||||
resp = api.read_namespaced_pod(namespace=namespace, name=pod)
|
|
||||||
return resp
|
|
||||||
|
|
||||||
def get_pod_logs(self, namespace, pod):
|
|
||||||
api = self.get_api()
|
|
||||||
log_content = api.read_namespaced_pod_log(pod, namespace, pretty=True, tail_lines=200)
|
|
||||||
return log_content
|
|
||||||
|
|
||||||
def get_pods(self):
|
|
||||||
api = self.get_api()
|
|
||||||
try:
|
|
||||||
ret = api.list_pod_for_all_namespaces(watch=False, _request_timeout=(3, 3))
|
|
||||||
except MaxRetryError:
|
|
||||||
logger.warning('Kubernetes connection timed out')
|
|
||||||
return
|
|
||||||
except ApiException as e:
|
|
||||||
if e.status == 401:
|
|
||||||
logger.warning('Kubernetes User not authenticated')
|
|
||||||
else:
|
|
||||||
logger.warning(e)
|
|
||||||
return
|
|
||||||
data = {}
|
|
||||||
for i in ret.items:
|
|
||||||
namespace = i.metadata.namespace
|
|
||||||
pod_info = {
|
|
||||||
'pod_name': i.metadata.name,
|
|
||||||
'containers': [j.name for j in i.spec.containers]
|
|
||||||
}
|
|
||||||
if namespace in data:
|
|
||||||
data[namespace].append(pod_info)
|
|
||||||
else:
|
|
||||||
data[namespace] = [pod_info, ]
|
|
||||||
return data
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_kubernetes_data(app_id, system_user_id):
|
|
||||||
from ..models import Application
|
|
||||||
app = get_object_or_404(Application, id=app_id)
|
|
||||||
system_user = get_object_or_404(SystemUser, id=system_user_id)
|
|
||||||
k8s = KubernetesClient(app.attrs['cluster'], system_user.token)
|
|
||||||
return k8s.get_pods()
|
|
||||||
|
|
||||||
|
|
||||||
class KubernetesTree:
|
|
||||||
def __init__(self, tree_id):
|
|
||||||
self.tree_id = tree_id
|
|
||||||
|
|
||||||
def as_tree_node(self, app):
|
|
||||||
pid = app.create_app_tree_pid(self.tree_id)
|
|
||||||
app_id = str(app.id)
|
|
||||||
parent_info = {'app_id': app_id}
|
|
||||||
node = self.create_tree_node(
|
|
||||||
app_id, pid, app.name, 'k8s', parent_info
|
|
||||||
)
|
|
||||||
return node
|
|
||||||
|
|
||||||
def as_system_user_tree_node(self, system_user, parent_info):
|
|
||||||
from ..models import ApplicationTreeNodeMixin
|
|
||||||
system_user_id = str(system_user.id)
|
|
||||||
username = system_user.username
|
|
||||||
username = username if username else '*'
|
|
||||||
name = f'{system_user.name}({username})'
|
|
||||||
pid = urlencode({'app_id': self.tree_id})
|
|
||||||
i = ApplicationTreeNodeMixin.create_tree_id(pid, 'system_user_id', system_user_id)
|
|
||||||
parent_info.update({'system_user_id': system_user_id})
|
|
||||||
node = self.create_tree_node(
|
|
||||||
i, pid, name, 'system_user', parent_info, icon='user-tie'
|
|
||||||
)
|
|
||||||
return node
|
|
||||||
|
|
||||||
def as_namespace_pod_tree_node(self, name, meta, type, counts=0, is_container=False):
|
|
||||||
from ..models import ApplicationTreeNodeMixin
|
|
||||||
i = ApplicationTreeNodeMixin.create_tree_id(self.tree_id, type, name)
|
|
||||||
meta.update({type: name})
|
|
||||||
name = name if is_container else f'{name}({counts})'
|
|
||||||
node = self.create_tree_node(
|
|
||||||
i, self.tree_id, name, type, meta, icon='cloud', is_container=is_container
|
|
||||||
)
|
|
||||||
return node
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def create_tree_node(id_, pid, name, identity, parent_info, icon='', is_container=False):
|
|
||||||
node = TreeNode(**{
|
|
||||||
'id': id_,
|
|
||||||
'name': name,
|
|
||||||
'title': name,
|
|
||||||
'pId': pid,
|
|
||||||
'isParent': not is_container,
|
|
||||||
'open': False,
|
|
||||||
'iconSkin': icon,
|
|
||||||
'parentInfo': urlencode(parent_info),
|
|
||||||
'meta': {
|
|
||||||
'type': 'application',
|
|
||||||
'data': {
|
|
||||||
'category': const.AppCategory.cloud,
|
|
||||||
'type': const.AppType.k8s,
|
|
||||||
'identity': identity
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return node
|
|
||||||
|
|
||||||
def async_tree_node(self, parent_info):
|
|
||||||
pod_name = parent_info.get('pod')
|
|
||||||
app_id = parent_info.get('app_id')
|
|
||||||
namespace = parent_info.get('namespace')
|
|
||||||
system_user_id = parent_info.get('system_user_id')
|
|
||||||
|
|
||||||
tree_nodes = []
|
|
||||||
data = KubernetesClient.get_kubernetes_data(app_id, system_user_id)
|
|
||||||
if not data:
|
|
||||||
return tree_nodes
|
|
||||||
|
|
||||||
if pod_name:
|
|
||||||
for container in next(
|
|
||||||
filter(
|
|
||||||
lambda x: x['pod_name'] == pod_name, data[namespace]
|
|
||||||
)
|
|
||||||
)['containers']:
|
|
||||||
container_node = self.as_namespace_pod_tree_node(
|
|
||||||
container, parent_info, 'container', is_container=True
|
|
||||||
)
|
|
||||||
tree_nodes.append(container_node)
|
|
||||||
elif namespace:
|
|
||||||
for pod in data[namespace]:
|
|
||||||
pod_nodes = self.as_namespace_pod_tree_node(
|
|
||||||
pod['pod_name'], parent_info, 'pod', len(pod['containers'])
|
|
||||||
)
|
|
||||||
tree_nodes.append(pod_nodes)
|
|
||||||
elif system_user_id:
|
|
||||||
for namespace, pods in data.items():
|
|
||||||
namespace_node = self.as_namespace_pod_tree_node(
|
|
||||||
namespace, parent_info, 'namespace', len(pods)
|
|
||||||
)
|
|
||||||
tree_nodes.append(namespace_node)
|
|
||||||
return tree_nodes
|
|
|
@ -5,7 +5,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from common.drf.serializers import JMSWritableNestedModelSerializer
|
from common.drf.serializers import JMSWritableNestedModelSerializer
|
||||||
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
|
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
|
||||||
from ...models import Asset, Node, Platform, Protocol, Label, Domain
|
from ...models import Asset, Node, Platform, Protocol, Label, Domain, Account
|
||||||
from ..mixin import CategoryDisplayMixin
|
from ..mixin import CategoryDisplayMixin
|
||||||
from ..account import AccountSerializer
|
from ..account import AccountSerializer
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ class AssetSerializer(CategoryDisplayMixin,
|
||||||
OrgResourceModelSerializerMixin):
|
OrgResourceModelSerializerMixin):
|
||||||
domain = AssetDomainSerializer(required=False)
|
domain = AssetDomainSerializer(required=False)
|
||||||
nodes_display = serializers.ListField(
|
nodes_display = serializers.ListField(
|
||||||
child=serializers.CharField(), label=_('Nodes name'), required=False
|
child=serializers.CharField(), label=_('Nodes name'), required=False,
|
||||||
)
|
)
|
||||||
labels = AssetLabelSerializer(many=True, required=False)
|
labels = AssetLabelSerializer(many=True, required=False)
|
||||||
nodes = AssetNodesSerializer(many=True, required=False)
|
nodes = AssetNodesSerializer(many=True, required=False)
|
||||||
|
@ -105,7 +105,6 @@ class AssetSerializer(CategoryDisplayMixin,
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def validate_type(self, value):
|
def validate_type(self, value):
|
||||||
print(self.initial_data)
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def validate_category(self, value):
|
def validate_category(self, value):
|
||||||
|
|
|
@ -21,7 +21,6 @@ api_v1 = [
|
||||||
path('settings/', include('settings.urls.api_urls', namespace='api-settings')),
|
path('settings/', include('settings.urls.api_urls', namespace='api-settings')),
|
||||||
path('authentication/', include('authentication.urls.api_urls', namespace='api-auth')),
|
path('authentication/', include('authentication.urls.api_urls', namespace='api-auth')),
|
||||||
path('common/', include('common.urls.api_urls', namespace='api-common')),
|
path('common/', include('common.urls.api_urls', namespace='api-common')),
|
||||||
path('applications/', include('applications.urls.api_urls', namespace='api-applications')),
|
|
||||||
path('tickets/', include('tickets.urls.api_urls', namespace='api-tickets')),
|
path('tickets/', include('tickets.urls.api_urls', namespace='api-tickets')),
|
||||||
path('acls/', include('acls.urls.api_urls', namespace='api-acls')),
|
path('acls/', include('acls.urls.api_urls', namespace='api-acls')),
|
||||||
path('notifications/', include('notifications.urls.api_urls', namespace='api-notifications')),
|
path('notifications/', include('notifications.urls.api_urls', namespace='api-notifications')),
|
||||||
|
|
|
@ -2,5 +2,4 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
from .asset import *
|
from .asset import *
|
||||||
from .application import *
|
|
||||||
from .system_user_permission import *
|
from .system_user_permission import *
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
from .user_permission import *
|
|
||||||
from .application_permission import *
|
|
||||||
from .application_permission_relation import *
|
|
||||||
from .user_group_permission import *
|
|
|
@ -1,55 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
from applications.models import Application
|
|
||||||
from perms.models import ApplicationPermission
|
|
||||||
from perms import serializers
|
|
||||||
from ..base import BasePermissionViewSet
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationPermissionViewSet(BasePermissionViewSet):
|
|
||||||
"""
|
|
||||||
应用授权列表的增删改查API
|
|
||||||
"""
|
|
||||||
model = ApplicationPermission
|
|
||||||
serializer_class = serializers.ApplicationPermissionSerializer
|
|
||||||
filterset_fields = {
|
|
||||||
'name': ['exact'],
|
|
||||||
'category': ['exact'],
|
|
||||||
'type': ['exact', 'in'],
|
|
||||||
'from_ticket': ['exact']
|
|
||||||
}
|
|
||||||
search_fields = ['name', 'category', 'type']
|
|
||||||
custom_filter_fields = BasePermissionViewSet.custom_filter_fields + [
|
|
||||||
'application_id', 'application', 'app', 'app_name'
|
|
||||||
]
|
|
||||||
ordering_fields = ('name',)
|
|
||||||
ordering = ('name', )
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
queryset = super().get_queryset().prefetch_related(
|
|
||||||
"applications", "users", "user_groups", "system_users"
|
|
||||||
)
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
def filter_application(self, queryset):
|
|
||||||
app_id = self.request.query_params.get('application_id') or \
|
|
||||||
self.request.query_params.get('app')
|
|
||||||
app_name = self.request.query_params.get('application') or \
|
|
||||||
self.request.query_params.get('app_name')
|
|
||||||
|
|
||||||
if app_id:
|
|
||||||
applications = Application.objects.filter(pk=app_id)
|
|
||||||
elif app_name:
|
|
||||||
applications = Application.objects.filter(name=app_name)
|
|
||||||
else:
|
|
||||||
return queryset
|
|
||||||
if not applications:
|
|
||||||
return queryset.none()
|
|
||||||
queryset = queryset.filter(applications__in=applications)
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
def filter_queryset(self, queryset):
|
|
||||||
queryset = super().filter_queryset(queryset)
|
|
||||||
queryset = self.filter_application(queryset)
|
|
||||||
return queryset
|
|
||||||
|
|
|
@ -1,123 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
from rest_framework import generics
|
|
||||||
from django.db.models import F, Value
|
|
||||||
from django.db.models.functions import Concat
|
|
||||||
from django.shortcuts import get_object_or_404
|
|
||||||
|
|
||||||
from applications.models import Application
|
|
||||||
from orgs.mixins.api import OrgRelationMixin
|
|
||||||
from orgs.mixins.api import OrgBulkModelViewSet
|
|
||||||
from orgs.utils import current_org
|
|
||||||
from perms import serializers
|
|
||||||
from perms import models
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'ApplicationPermissionUserRelationViewSet',
|
|
||||||
'ApplicationPermissionUserGroupRelationViewSet',
|
|
||||||
'ApplicationPermissionApplicationRelationViewSet',
|
|
||||||
'ApplicationPermissionSystemUserRelationViewSet',
|
|
||||||
'ApplicationPermissionAllApplicationListApi',
|
|
||||||
'ApplicationPermissionAllUserListApi',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class RelationMixin(OrgRelationMixin, OrgBulkModelViewSet):
|
|
||||||
perm_model = models.ApplicationPermission
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
queryset = super().get_queryset()
|
|
||||||
org_id = current_org.org_id()
|
|
||||||
if org_id is not None:
|
|
||||||
queryset = queryset.filter(applicationpermission__org_id=org_id)
|
|
||||||
queryset = queryset.annotate(applicationpermission_display=F('applicationpermission__name'))
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationPermissionUserRelationViewSet(RelationMixin):
|
|
||||||
serializer_class = serializers.ApplicationPermissionUserRelationSerializer
|
|
||||||
m2m_field = models.ApplicationPermission.users.field
|
|
||||||
filterset_fields = [
|
|
||||||
'id', "user", "applicationpermission",
|
|
||||||
]
|
|
||||||
search_fields = ("user__name", "user__username", "applicationpermission__name")
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
queryset = super().get_queryset()
|
|
||||||
queryset = queryset.annotate(user_display=F('user__name'))
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationPermissionUserGroupRelationViewSet(RelationMixin):
|
|
||||||
serializer_class = serializers.ApplicationPermissionUserGroupRelationSerializer
|
|
||||||
m2m_field = models.ApplicationPermission.user_groups.field
|
|
||||||
filterset_fields = [
|
|
||||||
'id', "usergroup", "applicationpermission"
|
|
||||||
]
|
|
||||||
search_fields = ["usergroup__name", "applicationpermission__name"]
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
queryset = super().get_queryset()
|
|
||||||
queryset = queryset.annotate(usergroup_display=F('usergroup__name'))
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationPermissionApplicationRelationViewSet(RelationMixin):
|
|
||||||
serializer_class = serializers.ApplicationPermissionApplicationRelationSerializer
|
|
||||||
m2m_field = models.ApplicationPermission.applications.field
|
|
||||||
filterset_fields = [
|
|
||||||
'id', 'application', 'applicationpermission',
|
|
||||||
]
|
|
||||||
search_fields = ["id", "application__name", "applicationpermission__name"]
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
queryset = super().get_queryset()
|
|
||||||
queryset = queryset.annotate(application_display=F('application__name'))
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationPermissionSystemUserRelationViewSet(RelationMixin):
|
|
||||||
serializer_class = serializers.ApplicationPermissionSystemUserRelationSerializer
|
|
||||||
m2m_field = models.ApplicationPermission.system_users.field
|
|
||||||
filterset_fields = [
|
|
||||||
'id', 'systemuser', 'applicationpermission',
|
|
||||||
]
|
|
||||||
search_fields = [
|
|
||||||
"applicactionpermission__name", "systemuser__name", "systemuser__username"
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
queryset = super().get_queryset()
|
|
||||||
queryset = queryset.annotate(
|
|
||||||
systemuser_display=Concat(
|
|
||||||
F('systemuser__name'), Value('('), F('systemuser__username'),
|
|
||||||
Value(')')
|
|
||||||
))
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationPermissionAllApplicationListApi(generics.ListAPIView):
|
|
||||||
serializer_class = serializers.ApplicationPermissionAllApplicationSerializer
|
|
||||||
only_fields = serializers.ApplicationPermissionAllApplicationSerializer.Meta.only_fields
|
|
||||||
filterset_fields = ('name',)
|
|
||||||
search_fields = filterset_fields
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
pk = self.kwargs.get('pk')
|
|
||||||
perm = get_object_or_404(models.ApplicationPermission, pk=pk)
|
|
||||||
applications = Application.objects.filter(granted_by_permissions=perm) \
|
|
||||||
.only(*self.only_fields).distinct()
|
|
||||||
return applications
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationPermissionAllUserListApi(generics.ListAPIView):
|
|
||||||
serializer_class = serializers.ApplicationPermissionAllUserSerializer
|
|
||||||
only_fields = serializers.ApplicationPermissionAllUserSerializer.Meta.only_fields
|
|
||||||
filterset_fields = ('username', 'name')
|
|
||||||
search_fields = filterset_fields
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
pk = self.kwargs.get('pk')
|
|
||||||
perm = get_object_or_404(models.ApplicationPermission, pk=pk)
|
|
||||||
users = perm.get_all_users().only(*self.only_fields).distinct()
|
|
||||||
return users
|
|
|
@ -1,36 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
|
|
||||||
from django.db.models import Q
|
|
||||||
from rest_framework.generics import ListAPIView
|
|
||||||
|
|
||||||
from common.mixins.api import CommonApiMixin
|
|
||||||
from applications.models import Application
|
|
||||||
from perms import serializers
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'UserGroupGrantedApplicationsApi'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class UserGroupGrantedApplicationsApi(CommonApiMixin, ListAPIView):
|
|
||||||
"""
|
|
||||||
获取用户组直接授权的应用
|
|
||||||
"""
|
|
||||||
serializer_class = serializers.AppGrantedSerializer
|
|
||||||
only_fields = serializers.AppGrantedSerializer.Meta.only_fields
|
|
||||||
filterset_fields = ['id', 'name', 'category', 'type', 'comment']
|
|
||||||
search_fields = ['name', 'comment']
|
|
||||||
rbac_perms = {
|
|
||||||
'list': 'perms.view_applicationpermission'
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
user_group_id = self.kwargs.get('pk')
|
|
||||||
if not user_group_id:
|
|
||||||
return Application.objects.none()
|
|
||||||
|
|
||||||
queryset = Application.objects\
|
|
||||||
.filter(Q(granted_by_permissions__user_groups__id=user_group_id))\
|
|
||||||
.distinct().only(*self.only_fields)
|
|
||||||
return queryset
|
|
|
@ -1,2 +0,0 @@
|
||||||
from .user_permission_applications import *
|
|
||||||
from .common import *
|
|
|
@ -1,83 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
import time
|
|
||||||
|
|
||||||
from django.shortcuts import get_object_or_404
|
|
||||||
from django.utils.decorators import method_decorator
|
|
||||||
from rest_framework.views import APIView, Response
|
|
||||||
from rest_framework import status
|
|
||||||
from rest_framework.generics import (
|
|
||||||
ListAPIView, get_object_or_404
|
|
||||||
)
|
|
||||||
|
|
||||||
from orgs.utils import tmp_to_root_org
|
|
||||||
from applications.models import Application
|
|
||||||
from perms.utils.application.permission import (
|
|
||||||
get_application_system_user_ids,
|
|
||||||
validate_permission,
|
|
||||||
)
|
|
||||||
from .mixin import AppRoleAdminMixin, AppRoleUserMixin
|
|
||||||
from perms.hands import User, SystemUser
|
|
||||||
from perms import serializers
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'UserGrantedApplicationSystemUsersApi',
|
|
||||||
'MyGrantedApplicationSystemUsersApi',
|
|
||||||
'ValidateUserApplicationPermissionApi'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class BaseGrantedApplicationSystemUsersApi(ListAPIView):
|
|
||||||
serializer_class = serializers.ApplicationSystemUserSerializer
|
|
||||||
only_fields = serializers.ApplicationSystemUserSerializer.Meta.only_fields
|
|
||||||
user: None
|
|
||||||
|
|
||||||
def get_application_system_user_ids(self, application):
|
|
||||||
return get_application_system_user_ids(self.user, application)
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
application_id = self.kwargs.get('application_id')
|
|
||||||
application = get_object_or_404(Application, id=application_id)
|
|
||||||
system_user_ids = self.get_application_system_user_ids(application)
|
|
||||||
system_users = SystemUser.objects.filter(id__in=system_user_ids) \
|
|
||||||
.only(*self.only_fields).order_by('priority')
|
|
||||||
return system_users
|
|
||||||
|
|
||||||
|
|
||||||
class UserGrantedApplicationSystemUsersApi(AppRoleAdminMixin, BaseGrantedApplicationSystemUsersApi):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class MyGrantedApplicationSystemUsersApi(AppRoleUserMixin, BaseGrantedApplicationSystemUsersApi):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(tmp_to_root_org(), name='get')
|
|
||||||
class ValidateUserApplicationPermissionApi(APIView):
|
|
||||||
rbac_perms = {
|
|
||||||
'GET': 'perms.view_applicationpermission'
|
|
||||||
}
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
user_id = request.query_params.get('user_id', '')
|
|
||||||
application_id = request.query_params.get('application_id', '')
|
|
||||||
account = system_user_id = request.query_params.get('account', '')
|
|
||||||
|
|
||||||
data = {
|
|
||||||
'has_permission': False,
|
|
||||||
'expire_at': int(time.time()),
|
|
||||||
'actions': []
|
|
||||||
}
|
|
||||||
if not all((user_id, application_id, account)):
|
|
||||||
return Response(data)
|
|
||||||
|
|
||||||
user = User.objects.get(id=user_id)
|
|
||||||
application = Application.objects.get(id=application_id)
|
|
||||||
has_perm, actions, expire_at = validate_permission(user, application, account)
|
|
||||||
status_code = status.HTTP_200_OK if has_perm else status.HTTP_403_FORBIDDEN
|
|
||||||
data = {
|
|
||||||
'has_permission': has_perm,
|
|
||||||
'expire_at': int(expire_at),
|
|
||||||
'actions': actions
|
|
||||||
}
|
|
||||||
return Response(data, status=status_code)
|
|
|
@ -1,24 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
|
|
||||||
from common.mixins.api import RoleAdminMixin as _RoleAdminMixin
|
|
||||||
from common.mixins.api import RoleUserMixin as _RoleUserMixin
|
|
||||||
from orgs.utils import tmp_to_root_org
|
|
||||||
|
|
||||||
|
|
||||||
class AppRoleAdminMixin(_RoleAdminMixin):
|
|
||||||
rbac_perms = (
|
|
||||||
('list', 'perms.view_userapp'),
|
|
||||||
('retrieve', 'perms.view_userapps'),
|
|
||||||
('get_tree', 'perms.view_userapps'),
|
|
||||||
('GET', 'perms.view_userapps'),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AppRoleUserMixin(_RoleUserMixin):
|
|
||||||
rbac_perms = (
|
|
||||||
('list', 'perms.view_myapps'),
|
|
||||||
('retrieve', 'perms.view_myapps'),
|
|
||||||
('get_tree', 'perms.view_myapps'),
|
|
||||||
('GET', 'perms.view_myapps'),
|
|
||||||
)
|
|
|
@ -1,81 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
from typing import Callable
|
|
||||||
|
|
||||||
from rest_framework.generics import ListAPIView
|
|
||||||
from rest_framework.response import Response
|
|
||||||
|
|
||||||
from common.mixins.api import CommonApiMixin
|
|
||||||
from common.tree import TreeNodeSerializer
|
|
||||||
from perms import serializers
|
|
||||||
from perms.tree.app import GrantedAppTreeUtil
|
|
||||||
from perms.utils.application.user_permission import (
|
|
||||||
get_user_granted_all_applications
|
|
||||||
)
|
|
||||||
from .mixin import AppRoleAdminMixin, AppRoleUserMixin
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'UserAllGrantedApplicationsApi',
|
|
||||||
'MyAllGrantedApplicationsApi',
|
|
||||||
'UserAllGrantedApplicationsAsTreeApi',
|
|
||||||
'MyAllGrantedApplicationsAsTreeApi',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class AllGrantedApplicationsApi(CommonApiMixin, ListAPIView):
|
|
||||||
only_fields = serializers.AppGrantedSerializer.Meta.only_fields
|
|
||||||
serializer_class = serializers.AppGrantedSerializer
|
|
||||||
filterset_fields = {
|
|
||||||
'id': ['exact'],
|
|
||||||
'name': ['exact'],
|
|
||||||
'category': ['exact'],
|
|
||||||
'type': ['exact', 'in'],
|
|
||||||
'comment': ['exact'],
|
|
||||||
}
|
|
||||||
search_fields = ['name', 'comment']
|
|
||||||
user: None
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
queryset = get_user_granted_all_applications(self.user)
|
|
||||||
return queryset.only(*self.only_fields)
|
|
||||||
|
|
||||||
|
|
||||||
class UserAllGrantedApplicationsApi(AppRoleAdminMixin, AllGrantedApplicationsApi):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class MyAllGrantedApplicationsApi(AppRoleUserMixin, AllGrantedApplicationsApi):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationsAsTreeMixin:
|
|
||||||
"""
|
|
||||||
将应用序列化成树的结构返回
|
|
||||||
"""
|
|
||||||
serializer_class = TreeNodeSerializer
|
|
||||||
user: None
|
|
||||||
filter_queryset: Callable
|
|
||||||
get_queryset: Callable
|
|
||||||
get_serializer: Callable
|
|
||||||
|
|
||||||
def list(self, request, *args, **kwargs):
|
|
||||||
tree_id = request.query_params.get('tree_id', None)
|
|
||||||
parent_info = request.query_params.get('parentInfo', None)
|
|
||||||
queryset = self.filter_queryset(self.get_queryset())
|
|
||||||
util = GrantedAppTreeUtil()
|
|
||||||
|
|
||||||
if not tree_id:
|
|
||||||
tree_nodes = util.create_tree_nodes(queryset)
|
|
||||||
else:
|
|
||||||
tree_nodes = util.get_children_nodes(tree_id, parent_info, self.user)
|
|
||||||
serializer = self.get_serializer(tree_nodes, many=True)
|
|
||||||
return Response(data=serializer.data)
|
|
||||||
|
|
||||||
|
|
||||||
class UserAllGrantedApplicationsAsTreeApi(ApplicationsAsTreeMixin, UserAllGrantedApplicationsApi):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class MyAllGrantedApplicationsAsTreeApi(ApplicationsAsTreeMixin, MyAllGrantedApplicationsApi):
|
|
||||||
pass
|
|
|
@ -2,5 +2,4 @@
|
||||||
#
|
#
|
||||||
from .base import *
|
from .base import *
|
||||||
from .asset import *
|
from .asset import *
|
||||||
from .application import *
|
|
||||||
from .system_user_permission import *
|
from .system_user_permission import *
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
from .permission import *
|
|
||||||
from .permission_relation import *
|
|
||||||
from .user_permission import *
|
|
|
@ -1,83 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
|
|
||||||
from rest_framework import serializers
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
|
||||||
from perms.models import ApplicationPermission, Action
|
|
||||||
from ..base import ActionsField, BasePermissionSerializer
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'ApplicationPermissionSerializer'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationPermissionSerializer(BasePermissionSerializer):
|
|
||||||
actions = ActionsField(required=False, allow_null=True, label=_("Actions"))
|
|
||||||
category_display = serializers.ReadOnlyField(source='get_category_display', label=_('Category display'))
|
|
||||||
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display'))
|
|
||||||
is_valid = serializers.BooleanField(read_only=True, label=_('Is valid'))
|
|
||||||
is_expired = serializers.BooleanField(read_only=True, label=_("Is expired"))
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = ApplicationPermission
|
|
||||||
fields_mini = ['id', 'name']
|
|
||||||
fields_small = fields_mini + [
|
|
||||||
'category', 'category_display', 'type', 'type_display',
|
|
||||||
'actions',
|
|
||||||
'is_active', 'is_expired', 'is_valid',
|
|
||||||
'created_by', 'date_created', 'date_expired', 'date_start', 'comment', 'from_ticket'
|
|
||||||
]
|
|
||||||
fields_m2m = [
|
|
||||||
'users', 'user_groups', 'applications', 'system_users',
|
|
||||||
'users_amount', 'user_groups_amount', 'applications_amount',
|
|
||||||
'system_users_amount',
|
|
||||||
]
|
|
||||||
fields = fields_small + fields_m2m
|
|
||||||
read_only_fields = ['created_by', 'date_created', 'from_ticket']
|
|
||||||
extra_kwargs = {
|
|
||||||
'is_expired': {'label': _('Is expired')},
|
|
||||||
'is_valid': {'label': _('Is valid')},
|
|
||||||
'actions': {'label': _('Actions')},
|
|
||||||
'users_amount': {'label': _('Users amount')},
|
|
||||||
'user_groups_amount': {'label': _('User groups amount')},
|
|
||||||
'system_users_amount': {'label': _('System users amount')},
|
|
||||||
'applications_amount': {'label': _('Apps amount')},
|
|
||||||
}
|
|
||||||
|
|
||||||
def _filter_actions_choices(self, choices):
|
|
||||||
if request := self.context.get('request'):
|
|
||||||
category = request.query_params.get('category')
|
|
||||||
else:
|
|
||||||
category = None
|
|
||||||
exclude_choices = ApplicationPermission.get_exclude_actions_choices(category=category)
|
|
||||||
for choice in exclude_choices:
|
|
||||||
choices.pop(choice, None)
|
|
||||||
return choices
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def setup_eager_loading(cls, queryset):
|
|
||||||
""" Perform necessary eager loading of data. """
|
|
||||||
queryset = queryset.prefetch_related(
|
|
||||||
'users', 'user_groups', 'applications', 'system_users'
|
|
||||||
)
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
def validate_applications(self, applications):
|
|
||||||
if self.instance:
|
|
||||||
permission_type = self.instance.type
|
|
||||||
else:
|
|
||||||
permission_type = self.initial_data['type']
|
|
||||||
|
|
||||||
other_type_applications = [
|
|
||||||
application for application in applications
|
|
||||||
if application.type != permission_type
|
|
||||||
]
|
|
||||||
if len(other_type_applications) > 0:
|
|
||||||
error = _(
|
|
||||||
'The application list contains applications '
|
|
||||||
'that are different from the permission type. ({})'
|
|
||||||
).format(', '.join([application.name for application in other_type_applications]))
|
|
||||||
raise serializers.ValidationError(error)
|
|
||||||
return applications
|
|
|
@ -1,88 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
from rest_framework import serializers
|
|
||||||
|
|
||||||
from common.mixins import BulkSerializerMixin
|
|
||||||
from perms.models import ApplicationPermission
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'ApplicationPermissionUserRelationSerializer',
|
|
||||||
'ApplicationPermissionUserGroupRelationSerializer',
|
|
||||||
'ApplicationPermissionApplicationRelationSerializer',
|
|
||||||
'ApplicationPermissionSystemUserRelationSerializer',
|
|
||||||
'ApplicationPermissionAllApplicationSerializer',
|
|
||||||
'ApplicationPermissionAllUserSerializer'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class RelationMixin(BulkSerializerMixin, serializers.Serializer):
|
|
||||||
applicationpermission_display = serializers.ReadOnlyField()
|
|
||||||
|
|
||||||
def get_field_names(self, declared_fields, info):
|
|
||||||
fields = super().get_field_names(declared_fields, info)
|
|
||||||
fields.extend(['applicationpermission', "applicationpermission_display"])
|
|
||||||
return fields
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationPermissionUserRelationSerializer(RelationMixin, serializers.ModelSerializer):
|
|
||||||
user_display = serializers.ReadOnlyField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = ApplicationPermission.users.through
|
|
||||||
fields = [
|
|
||||||
'id', 'user', 'user_display',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationPermissionUserGroupRelationSerializer(RelationMixin, serializers.ModelSerializer):
|
|
||||||
usergroup_display = serializers.ReadOnlyField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = ApplicationPermission.user_groups.through
|
|
||||||
fields = [
|
|
||||||
'id', 'usergroup', "usergroup_display",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationPermissionApplicationRelationSerializer(RelationMixin, serializers.ModelSerializer):
|
|
||||||
application_display = serializers.ReadOnlyField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = ApplicationPermission.applications.through
|
|
||||||
fields = [
|
|
||||||
'id', "application", "application_display",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationPermissionSystemUserRelationSerializer(RelationMixin, serializers.ModelSerializer):
|
|
||||||
systemuser_display = serializers.ReadOnlyField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = ApplicationPermission.system_users.through
|
|
||||||
fields = [
|
|
||||||
'id', 'systemuser', 'systemuser_display'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationPermissionAllApplicationSerializer(serializers.Serializer):
|
|
||||||
application = serializers.UUIDField(read_only=True, source='id')
|
|
||||||
application_display = serializers.SerializerMethodField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
only_fields = ['id', 'name']
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_application_display(obj):
|
|
||||||
return str(obj)
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationPermissionAllUserSerializer(serializers.Serializer):
|
|
||||||
user = serializers.UUIDField(read_only=True, source='id')
|
|
||||||
user_display = serializers.SerializerMethodField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
only_fields = ['id', 'username', 'name']
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_user_display(obj):
|
|
||||||
return str(obj)
|
|
|
@ -1,42 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
|
|
||||||
from rest_framework import serializers
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
from assets.models import SystemUser
|
|
||||||
from applications.models import Application
|
|
||||||
from applications.serializers import AppSerializerMixin
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'AppGrantedSerializer', 'ApplicationSystemUserSerializer'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationSystemUserSerializer(serializers.ModelSerializer):
|
|
||||||
"""
|
|
||||||
查看授权的应用系统用户的数据结构,这个和SystemUserSerializer不同,字段少
|
|
||||||
"""
|
|
||||||
class Meta:
|
|
||||||
model = SystemUser
|
|
||||||
only_fields = (
|
|
||||||
'id', 'name', 'username', 'priority', 'protocol', 'login_mode'
|
|
||||||
)
|
|
||||||
fields = list(only_fields)
|
|
||||||
read_only_fields = fields
|
|
||||||
|
|
||||||
|
|
||||||
class AppGrantedSerializer(AppSerializerMixin, serializers.ModelSerializer):
|
|
||||||
"""
|
|
||||||
被授权应用的数据结构
|
|
||||||
"""
|
|
||||||
category_display = serializers.ReadOnlyField(source='get_category_display', label=_('Category'))
|
|
||||||
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type'))
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Application
|
|
||||||
only_fields = [
|
|
||||||
'id', 'name', 'domain', 'category', 'type', 'attrs', 'comment', 'org_id'
|
|
||||||
]
|
|
||||||
fields = only_fields + ['category_display', 'type_display', 'org_name']
|
|
||||||
read_only_fields = fields
|
|
|
@ -1,3 +1,2 @@
|
||||||
from . import asset_permission
|
from . import asset_permission
|
||||||
from . import app_permission
|
|
||||||
from . import refresh_perms
|
from . import refresh_perms
|
||||||
|
|
|
@ -1,106 +0,0 @@
|
||||||
import itertools
|
|
||||||
|
|
||||||
from django.db.models.signals import m2m_changed
|
|
||||||
from django.dispatch import receiver
|
|
||||||
|
|
||||||
from users.models import User, UserGroup
|
|
||||||
from assets.models import Asset, SystemUser
|
|
||||||
from applications.models import Application
|
|
||||||
from common.utils import get_logger
|
|
||||||
from common.exceptions import M2MReverseNotAllowed
|
|
||||||
from common.decorator import on_transaction_commit
|
|
||||||
from common.const.signals import POST_ADD
|
|
||||||
from perms.models import ApplicationPermission
|
|
||||||
from applications.models import Account as AppAccount
|
|
||||||
|
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(m2m_changed, sender=ApplicationPermission.applications.through)
|
|
||||||
@on_transaction_commit
|
|
||||||
def on_app_permission_applications_changed(sender, instance, action, reverse, pk_set, **kwargs):
|
|
||||||
if reverse:
|
|
||||||
raise M2MReverseNotAllowed
|
|
||||||
if action != POST_ADD:
|
|
||||||
return
|
|
||||||
|
|
||||||
logger.debug("Application permission applications change signal received")
|
|
||||||
system_users = instance.system_users.all()
|
|
||||||
set_remote_app_asset_system_users_if_need(instance, system_users=system_users)
|
|
||||||
|
|
||||||
apps = Application.objects.filter(pk__in=pk_set)
|
|
||||||
set_app_accounts(apps, system_users)
|
|
||||||
|
|
||||||
|
|
||||||
def set_app_accounts(apps, system_users):
|
|
||||||
for app, system_user in itertools.product(apps, system_users):
|
|
||||||
AppAccount.objects.get_or_create(
|
|
||||||
defaults={'app': app, 'systemuser': system_user},
|
|
||||||
app=app, systemuser=system_user
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def set_remote_app_asset_system_users_if_need(instance: ApplicationPermission, system_users=None,
|
|
||||||
users=None, groups=None):
|
|
||||||
if not instance.category_remote_app:
|
|
||||||
return
|
|
||||||
|
|
||||||
attrs = instance.applications.all().values_list('attrs', flat=True)
|
|
||||||
asset_ids = [attr['asset'] for attr in attrs if attr.get('asset')]
|
|
||||||
# 远程应用中资产可能在资产表里不存在
|
|
||||||
asset_ids = Asset.objects.filter(id__in=asset_ids).values_list('id', flat=True)
|
|
||||||
if not asset_ids:
|
|
||||||
return
|
|
||||||
|
|
||||||
system_users = system_users or instance.system_users.all()
|
|
||||||
for system_user in system_users:
|
|
||||||
system_user.add_related_assets(asset_ids)
|
|
||||||
|
|
||||||
if system_user.username_same_with_user:
|
|
||||||
users = users or instance.users.all()
|
|
||||||
groups = groups or instance.user_groups.all()
|
|
||||||
system_user.groups.add(*groups)
|
|
||||||
system_user.users.add(*users)
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(m2m_changed, sender=ApplicationPermission.system_users.through)
|
|
||||||
@on_transaction_commit
|
|
||||||
def on_app_permission_system_users_changed(sender, instance, action, reverse, pk_set, **kwargs):
|
|
||||||
if reverse:
|
|
||||||
raise M2MReverseNotAllowed
|
|
||||||
if action != POST_ADD:
|
|
||||||
return
|
|
||||||
|
|
||||||
logger.debug("Application permission system_users change signal received")
|
|
||||||
system_users = SystemUser.objects.filter(pk__in=pk_set)
|
|
||||||
|
|
||||||
set_remote_app_asset_system_users_if_need(instance, system_users=system_users)
|
|
||||||
apps = instance.applications.all()
|
|
||||||
set_app_accounts(apps, system_users)
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(m2m_changed, sender=ApplicationPermission.users.through)
|
|
||||||
@on_transaction_commit
|
|
||||||
def on_app_permission_users_changed(sender, instance, action, reverse, pk_set, **kwargs):
|
|
||||||
if reverse:
|
|
||||||
raise M2MReverseNotAllowed
|
|
||||||
if action != POST_ADD:
|
|
||||||
return
|
|
||||||
|
|
||||||
logger.debug("Application permission users change signal received")
|
|
||||||
users = User.objects.filter(pk__in=pk_set)
|
|
||||||
set_remote_app_asset_system_users_if_need(instance, users=users)
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(m2m_changed, sender=ApplicationPermission.user_groups.through)
|
|
||||||
@on_transaction_commit
|
|
||||||
def on_app_permission_user_groups_changed(sender, instance, action, reverse, pk_set, **kwargs):
|
|
||||||
if reverse:
|
|
||||||
raise M2MReverseNotAllowed
|
|
||||||
if action != POST_ADD:
|
|
||||||
return
|
|
||||||
|
|
||||||
logger.debug("Application permission user groups change signal received")
|
|
||||||
groups = UserGroup.objects.filter(pk__in=pk_set)
|
|
||||||
set_remote_app_asset_system_users_if_need(instance, groups=groups)
|
|
|
@ -1,103 +0,0 @@
|
||||||
from urllib.parse import urlencode, parse_qsl
|
|
||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
|
||||||
from rest_framework.generics import get_object_or_404
|
|
||||||
|
|
||||||
from common.tree import TreeNode
|
|
||||||
from orgs.models import Organization
|
|
||||||
from assets.models import SystemUser
|
|
||||||
from applications.utils import KubernetesTree
|
|
||||||
from applications.models import Application
|
|
||||||
from perms.utils.application.permission import get_application_system_user_ids
|
|
||||||
|
|
||||||
|
|
||||||
class GrantedAppTreeUtil:
|
|
||||||
@staticmethod
|
|
||||||
def filter_organizations(applications):
|
|
||||||
organization_ids = set(applications.values_list('org_id', flat=True))
|
|
||||||
organizations = [Organization.get_instance(org_id) for org_id in organization_ids]
|
|
||||||
organizations.sort(key=lambda x: x.name)
|
|
||||||
return organizations
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def create_root_node():
|
|
||||||
name = _('My applications')
|
|
||||||
node = TreeNode(**{
|
|
||||||
'id': 'applications',
|
|
||||||
'name': name,
|
|
||||||
'title': name,
|
|
||||||
'pId': '',
|
|
||||||
'open': True,
|
|
||||||
'iconSkin': 'applications',
|
|
||||||
'isParent': True,
|
|
||||||
'meta': {
|
|
||||||
'type': 'root'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return node
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def create_empty_node():
|
|
||||||
name = _("Empty")
|
|
||||||
node = TreeNode(**{
|
|
||||||
'id': 'empty',
|
|
||||||
'name': name,
|
|
||||||
'title': name,
|
|
||||||
'pId': '',
|
|
||||||
'isParent': True,
|
|
||||||
'children': [],
|
|
||||||
'meta': {
|
|
||||||
'type': 'application'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return node
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_children_nodes(tree_id, parent_info, user):
|
|
||||||
tree_nodes = []
|
|
||||||
parent_info = dict(parse_qsl(parent_info))
|
|
||||||
pod_name = parent_info.get('pod')
|
|
||||||
app_id = parent_info.get('app_id')
|
|
||||||
namespace = parent_info.get('namespace')
|
|
||||||
system_user_id = parent_info.get('system_user_id')
|
|
||||||
|
|
||||||
if app_id and not any([pod_name, namespace, system_user_id]):
|
|
||||||
app = get_object_or_404(Application, id=app_id)
|
|
||||||
system_user_ids = get_application_system_user_ids(user, app)
|
|
||||||
system_users = SystemUser.objects.filter(id__in=system_user_ids).order_by('priority')
|
|
||||||
for system_user in system_users:
|
|
||||||
system_user_node = KubernetesTree(tree_id).as_system_user_tree_node(
|
|
||||||
system_user, parent_info
|
|
||||||
)
|
|
||||||
tree_nodes.append(system_user_node)
|
|
||||||
return tree_nodes
|
|
||||||
tree_nodes = KubernetesTree(tree_id).async_tree_node(parent_info)
|
|
||||||
return tree_nodes
|
|
||||||
|
|
||||||
def create_tree_nodes(self, applications):
|
|
||||||
tree_nodes = []
|
|
||||||
if not applications:
|
|
||||||
return [self.create_empty_node()]
|
|
||||||
|
|
||||||
root_node = self.create_root_node()
|
|
||||||
organizations = self.filter_organizations(applications)
|
|
||||||
|
|
||||||
for i, org in enumerate(organizations):
|
|
||||||
tree_id = urlencode({'org_id': str(org.id)})
|
|
||||||
apps = applications.filter(org_id=org.id)
|
|
||||||
|
|
||||||
# 组织节点
|
|
||||||
org_node = org.as_tree_node(oid=tree_id, pid=root_node.id)
|
|
||||||
org_node.name += '({})'.format(apps.count())
|
|
||||||
tree_nodes.append(org_node)
|
|
||||||
|
|
||||||
# 类别节点
|
|
||||||
category_type_nodes = Application.create_category_type_tree_nodes(
|
|
||||||
apps, tree_id, show_empty=False
|
|
||||||
)
|
|
||||||
tree_nodes += category_type_nodes
|
|
||||||
|
|
||||||
for app in apps:
|
|
||||||
app_node = app.as_tree_node(tree_id, k8s_as_tree=True)
|
|
||||||
tree_nodes.append(app_node)
|
|
||||||
return tree_nodes
|
|
|
@ -1,19 +1,8 @@
|
||||||
# coding:utf-8
|
# coding:utf-8
|
||||||
|
|
||||||
from django.urls import re_path
|
|
||||||
from common import api as capi
|
|
||||||
from .asset_permission import asset_permission_urlpatterns
|
from .asset_permission import asset_permission_urlpatterns
|
||||||
from .application_permission import application_permission_urlpatterns
|
|
||||||
from .system_user_permission import system_users_permission_urlpatterns
|
|
||||||
|
|
||||||
app_name = 'perms'
|
app_name = 'perms'
|
||||||
|
|
||||||
old_version_urlpatterns = [
|
|
||||||
re_path('(?P<resource>user|user-group|asset-permission|remote-app-permission)/.*', capi.redirect_plural_name_api)
|
|
||||||
]
|
|
||||||
|
|
||||||
urlpatterns = []
|
urlpatterns = []
|
||||||
urlpatterns += asset_permission_urlpatterns
|
urlpatterns += asset_permission_urlpatterns
|
||||||
urlpatterns += application_permission_urlpatterns
|
|
||||||
urlpatterns += system_users_permission_urlpatterns
|
|
||||||
urlpatterns += old_version_urlpatterns
|
|
||||||
|
|
|
@ -1,48 +0,0 @@
|
||||||
# coding: utf-8
|
|
||||||
#
|
|
||||||
|
|
||||||
from django.urls import path, include
|
|
||||||
from rest_framework_bulk.routes import BulkRouter
|
|
||||||
from .. import api
|
|
||||||
|
|
||||||
|
|
||||||
router = BulkRouter()
|
|
||||||
router.register('application-permissions', api.ApplicationPermissionViewSet, 'application-permission')
|
|
||||||
router.register('application-permissions-users-relations', api.ApplicationPermissionUserRelationViewSet, 'application-permissions-users-relation')
|
|
||||||
router.register('application-permissions-user-groups-relations', api.ApplicationPermissionUserGroupRelationViewSet, 'application-permissions-user-groups-relation')
|
|
||||||
router.register('application-permissions-applications-relations', api.ApplicationPermissionApplicationRelationViewSet, 'application-permissions-application-relation')
|
|
||||||
router.register('application-permissions-system-users-relations', api.ApplicationPermissionSystemUserRelationViewSet, 'application-permissions-system-users-relation')
|
|
||||||
|
|
||||||
user_permission_urlpatterns = [
|
|
||||||
path('<uuid:pk>/applications/', api.UserAllGrantedApplicationsApi.as_view(), name='user-applications'),
|
|
||||||
path('applications/', api.MyAllGrantedApplicationsApi.as_view(), name='my-applications'),
|
|
||||||
|
|
||||||
# Application As Tree
|
|
||||||
path('<uuid:pk>/applications/tree/', api.UserAllGrantedApplicationsAsTreeApi.as_view(), name='user-applications-as-tree'),
|
|
||||||
path('applications/tree/', api.MyAllGrantedApplicationsAsTreeApi.as_view(), name='my-applications-as-tree'),
|
|
||||||
|
|
||||||
# Application System Users
|
|
||||||
path('<uuid:pk>/applications/<uuid:application_id>/system-users/', api.UserGrantedApplicationSystemUsersApi.as_view(), name='user-application-system-users'),
|
|
||||||
path('applications/<uuid:application_id>/system-users/', api.MyGrantedApplicationSystemUsersApi.as_view(), name='my-application-system-users'),
|
|
||||||
]
|
|
||||||
|
|
||||||
user_group_permission_urlpatterns = [
|
|
||||||
path('<uuid:pk>/applications/', api.UserGroupGrantedApplicationsApi.as_view(), name='user-group-applications'),
|
|
||||||
]
|
|
||||||
|
|
||||||
permission_urlpatterns = [
|
|
||||||
# 授权规则中授权的用户和应用
|
|
||||||
path('<uuid:pk>/applications/all/', api.ApplicationPermissionAllApplicationListApi.as_view(), name='application-permission-all-applications'),
|
|
||||||
path('<uuid:pk>/users/all/', api.ApplicationPermissionAllUserListApi.as_view(), name='application-permission-all-users'),
|
|
||||||
|
|
||||||
# 验证用户是否有某个应用的权限
|
|
||||||
path('user/validate/', api.ValidateUserApplicationPermissionApi.as_view(), name='validate-user-application-permission'),
|
|
||||||
]
|
|
||||||
|
|
||||||
application_permission_urlpatterns = [
|
|
||||||
path('users/', include(user_permission_urlpatterns)),
|
|
||||||
path('user-groups/', include(user_group_permission_urlpatterns)),
|
|
||||||
path('application-permissions/', include(permission_urlpatterns))
|
|
||||||
]
|
|
||||||
|
|
||||||
application_permission_urlpatterns += router.urls
|
|
|
@ -1,6 +0,0 @@
|
||||||
from django.urls import path
|
|
||||||
from .. import api
|
|
||||||
|
|
||||||
system_users_permission_urlpatterns = [
|
|
||||||
path('system-users-permission/', api.SystemUserPermission.as_view(), name='system-users-permission'),
|
|
||||||
]
|
|
Loading…
Reference in New Issue