mirror of https://github.com/jumpserver/jumpserver
perf: merge v3
commit
1e57a0eb1f
|
@ -7,5 +7,6 @@ from .node import *
|
|||
from .domain import *
|
||||
from .gathered_user import *
|
||||
from .favorite_asset import *
|
||||
from .account_template import *
|
||||
from .account_backup import *
|
||||
from .account_history import *
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from ..models import AccountTemplate
|
||||
from .. import serializers
|
||||
|
||||
|
||||
class AccountTemplateViewSet(OrgBulkModelViewSet):
|
||||
model = AccountTemplate
|
||||
filterset_fields = ("username", 'name')
|
||||
search_fields = ('username', 'name')
|
||||
serializer_classes = {
|
||||
'default': serializers.AccountTemplateSerializer
|
||||
}
|
|
@ -16,5 +16,5 @@ class GatheredUserViewSet(OrgModelViewSet):
|
|||
serializer_class = GatheredUserSerializer
|
||||
extra_filter_backends = [AssetRelatedByNodeFilterBackend]
|
||||
|
||||
filterset_fields = ['asset', 'username', 'present', 'asset__ip', 'asset__hostname', 'asset_id']
|
||||
search_fields = ['username', 'asset__ip', 'asset__hostname']
|
||||
filterset_fields = ['asset', 'username', 'present', 'asset__ip', 'asset__name', 'asset_id']
|
||||
search_fields = ['username', 'asset__ip', 'asset__name']
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from common.db.models import IncludesTextChoicesMeta
|
||||
from common.db.models import IncludesTextChoicesMeta, ChoicesMixin
|
||||
from common.tree import TreeNode
|
||||
|
||||
|
||||
|
@ -24,7 +24,7 @@ class PlatformMixin:
|
|||
}
|
||||
|
||||
|
||||
class Category(PlatformMixin, models.TextChoices):
|
||||
class Category(PlatformMixin, ChoicesMixin, models.TextChoices):
|
||||
HOST = 'host', _('Host')
|
||||
NETWORK = 'network', _("NetworkDevice")
|
||||
DATABASE = 'database', _("Database")
|
||||
|
@ -60,7 +60,7 @@ class Category(PlatformMixin, models.TextChoices):
|
|||
}
|
||||
|
||||
|
||||
class HostTypes(PlatformMixin, models.TextChoices):
|
||||
class HostTypes(PlatformMixin, ChoicesMixin, models.TextChoices):
|
||||
LINUX = 'linux', 'Linux'
|
||||
WINDOWS = 'windows', 'Windows'
|
||||
UNIX = 'unix', 'Unix'
|
||||
|
@ -84,14 +84,14 @@ class HostTypes(PlatformMixin, models.TextChoices):
|
|||
}
|
||||
|
||||
|
||||
class NetworkTypes(PlatformMixin, models.TextChoices):
|
||||
class NetworkTypes(PlatformMixin, ChoicesMixin, models.TextChoices):
|
||||
SWITCH = 'switch', _("Switch")
|
||||
ROUTER = 'router', _("Router")
|
||||
FIREWALL = 'firewall', _("Firewall")
|
||||
OTHER_NETWORK = 'other_network', _("Other device")
|
||||
|
||||
|
||||
class DatabaseTypes(PlatformMixin, models.TextChoices):
|
||||
class DatabaseTypes(PlatformMixin, ChoicesMixin, models.TextChoices):
|
||||
MYSQL = 'mysql', 'MySQL'
|
||||
MARIADB = 'mariadb', 'MariaDB'
|
||||
POSTGRESQL = 'postgresql', 'PostgreSQL'
|
||||
|
@ -110,15 +110,15 @@ class DatabaseTypes(PlatformMixin, models.TextChoices):
|
|||
return meta
|
||||
|
||||
|
||||
class WebTypes(PlatformMixin, models.TextChoices):
|
||||
class WebTypes(PlatformMixin, ChoicesMixin, models.TextChoices):
|
||||
General = 'general', 'General'
|
||||
|
||||
|
||||
class CloudTypes(PlatformMixin, models.TextChoices):
|
||||
class CloudTypes(PlatformMixin, ChoicesMixin, models.TextChoices):
|
||||
K8S = 'k8s', 'Kubernetes'
|
||||
|
||||
|
||||
class AllTypes(metaclass=IncludesTextChoicesMeta):
|
||||
class AllTypes(ChoicesMixin, metaclass=IncludesTextChoicesMeta):
|
||||
choices: list
|
||||
includes = [
|
||||
HostTypes, NetworkTypes, DatabaseTypes,
|
||||
|
@ -202,7 +202,7 @@ class AllTypes(metaclass=IncludesTextChoicesMeta):
|
|||
return nodes
|
||||
|
||||
|
||||
class Protocol(models.TextChoices):
|
||||
class Protocol(ChoicesMixin, models.TextChoices):
|
||||
ssh = 'ssh', 'SSH'
|
||||
rdp = 'rdp', 'RDP'
|
||||
telnet = 'telnet', 'Telnet'
|
||||
|
|
|
@ -18,6 +18,9 @@ def create_app_platform(apps, *args):
|
|||
{'name': 'MongoDB', 'category': 'database', 'type': 'mongodb'},
|
||||
{'name': 'Redis', 'category': 'database', 'type': 'redis'},
|
||||
{'name': 'Chrome', 'category': 'remote_app', 'type': 'chrome'},
|
||||
{'name': 'MysqlWorkbench', 'category': 'remote_app', 'type': 'mysql_workbench'},
|
||||
{'name': 'VmwareClient', 'category': 'remote_app', 'type': 'vmware_client'},
|
||||
{'name': 'General', 'category': 'remote_app', 'type': 'general_remote_app'},
|
||||
{'name': 'Kubernetes', 'category': 'cloud', 'type': 'k8s'},
|
||||
]
|
||||
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
# Generated by Django 3.2.13 on 2022-08-19 07:23
|
||||
|
||||
import assets.models.base
|
||||
import common.db.fields
|
||||
from django.db import migrations, models
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0110_auto_20220817_1716'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AccountTemplate',
|
||||
fields=[
|
||||
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
|
||||
('connectivity', models.CharField(choices=[('unknown', 'Unknown'), ('ok', 'Ok'), ('failed', 'Failed')], default='unknown', max_length=16, verbose_name='Connectivity')),
|
||||
('date_verified', models.DateTimeField(null=True, verbose_name='Date verified')),
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||
('username', models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username')),
|
||||
('password', common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
|
||||
('private_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
|
||||
('public_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
|
||||
('comment', models.TextField(blank=True, verbose_name='Comment')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')),
|
||||
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||
('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')),
|
||||
('token', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Token')),
|
||||
('privileged', models.BooleanField(default=False, verbose_name='Privileged account')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Account template',
|
||||
},
|
||||
bases=(models.Model, assets.models.base.AuthMixin),
|
||||
)
|
||||
]
|
|
@ -0,0 +1,22 @@
|
|||
# Generated by Django 3.2.13 on 2022-08-19 07:26
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0111_auto_20220819_1523'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='platform',
|
||||
name='protocols_default',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='platform',
|
||||
name='protocols_enabled',
|
||||
field=models.BooleanField(default=True, verbose_name='Protocols enabled'),
|
||||
),
|
||||
]
|
|
@ -5,7 +5,7 @@ from simple_history.models import HistoricalRecords
|
|||
from common.db import fields
|
||||
from .base import BaseAccount, AbsConnectivity
|
||||
|
||||
__all__ = ['Account']
|
||||
__all__ = ['Account', 'AccountTemplate']
|
||||
|
||||
|
||||
class Account(BaseAccount, AbsConnectivity):
|
||||
|
@ -27,3 +27,14 @@ class Account(BaseAccount, AbsConnectivity):
|
|||
|
||||
def __str__(self):
|
||||
return '{}@{}'.format(self.username, self.asset.name)
|
||||
|
||||
|
||||
class AccountTemplate(BaseAccount, AbsConnectivity):
|
||||
token = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Token'))
|
||||
privileged = models.BooleanField(verbose_name=_("Privileged account"), default=False)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Account template')
|
||||
|
||||
def __str__(self):
|
||||
return '{}@{}'.format(self.username, self.name)
|
||||
|
|
|
@ -11,6 +11,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||
|
||||
from common.utils import lazyproperty
|
||||
from orgs.mixins.models import OrgManager, JMSOrgBaseModel
|
||||
from ...const import Category
|
||||
from ..platform import Platform
|
||||
from ..base import AbsConnectivity
|
||||
|
||||
|
|
|
@ -9,5 +9,6 @@ from .gathered_user import *
|
|||
from .favorite_asset import *
|
||||
from .account import *
|
||||
from .account_history import *
|
||||
from .account_template import *
|
||||
from .backup import *
|
||||
from .platform import *
|
||||
|
|
|
@ -6,7 +6,6 @@ from assets.models import Account
|
|||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
|
||||
from .base import AuthSerializerMixin
|
||||
from common.utils.encode import ssh_pubkey_gen
|
||||
from common.drf.serializers import SecretReadableMixin
|
||||
|
||||
|
||||
|
@ -33,17 +32,6 @@ class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
|||
}
|
||||
ref_name = 'AssetAccountSerializer'
|
||||
|
||||
def _validate_gen_key(self, attrs):
|
||||
private_key = attrs.get('private_key')
|
||||
if not private_key:
|
||||
return attrs
|
||||
|
||||
password = attrs.get('passphrase')
|
||||
username = attrs.get('username')
|
||||
public_key = ssh_pubkey_gen(private_key, password=password, username=username)
|
||||
attrs['public_key'] = public_key
|
||||
return attrs
|
||||
|
||||
def validate(self, attrs):
|
||||
attrs = self._validate_gen_key(attrs)
|
||||
return attrs
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
from assets.models import AccountTemplate
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
|
||||
from .base import AuthSerializerMixin
|
||||
from .account import AccountSerializer
|
||||
|
||||
|
||||
class AccountTemplateSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
class Meta:
|
||||
model = AccountTemplate
|
||||
fields_mini = ['id', 'privileged', 'username', 'name']
|
||||
fields_write_only = AccountSerializer.Meta.fields_write_only
|
||||
fields_other = AccountSerializer.Meta.fields_other
|
||||
fields = fields_mini + fields_write_only + fields_other
|
||||
extra_kwargs = AccountSerializer.Meta.extra_kwargs
|
||||
|
||||
def validate(self, attrs):
|
||||
print(attrs)
|
||||
|
||||
raise ValueError('test')
|
||||
attrs = self._validate_gen_key(attrs)
|
||||
return attrs
|
|
@ -5,6 +5,7 @@ from .common import AssetSerializer
|
|||
|
||||
__all__ = [
|
||||
'DeviceSerializer', 'HostSerializer', 'DatabaseSerializer',
|
||||
'NetworkSerializer', 'CloudSerializer',
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -13,7 +13,8 @@ from .utils import validate_password_for_ansible
|
|||
|
||||
class AuthSerializer(serializers.ModelSerializer):
|
||||
password = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=1024, label=_('Password'))
|
||||
private_key = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=16384, label=_('Private key'))
|
||||
private_key = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=16384,
|
||||
label=_('Private key'))
|
||||
|
||||
def gen_keys(self, private_key=None, password=None):
|
||||
if private_key is None:
|
||||
|
@ -74,6 +75,18 @@ class AuthSerializerMixin(serializers.ModelSerializer):
|
|||
validated_data.pop(field, None)
|
||||
validated_data.pop('passphrase', None)
|
||||
|
||||
@staticmethod
|
||||
def _validate_gen_key(attrs):
|
||||
private_key = attrs.get('private_key')
|
||||
if not private_key:
|
||||
return attrs
|
||||
|
||||
password = attrs.get('passphrase')
|
||||
username = attrs.get('username')
|
||||
public_key = ssh_pubkey_gen(private_key, password=password, username=username)
|
||||
attrs['public_key'] = public_key
|
||||
return attrs
|
||||
|
||||
def create(self, validated_data):
|
||||
self.clean_auth_fields(validated_data)
|
||||
return super().create(validated_data)
|
||||
|
|
|
@ -14,6 +14,7 @@ router.register(r'assets', api.AssetViewSet, 'asset')
|
|||
router.register(r'hosts', api.HostViewSet, 'host')
|
||||
router.register(r'databases', api.DatabaseViewSet, 'database')
|
||||
router.register(r'accounts', api.AccountViewSet, 'account')
|
||||
router.register(r'account-templates', api.AccountTemplateViewSet, 'account-template')
|
||||
router.register(r'account-secrets', api.AccountSecretsViewSet, 'account-secret')
|
||||
router.register(r'accounts-history', api.AccountHistoryViewSet, 'account-history')
|
||||
router.register(r'account-history-secrets', api.AccountHistorySecretsViewSet, 'account-history-secret')
|
||||
|
|
|
@ -107,11 +107,11 @@ class CommandExecutionViewSet(ListModelMixin, OrgGenericViewSet):
|
|||
]
|
||||
filterset_fields = [
|
||||
'user__name', 'user__username', 'command',
|
||||
'run_as__name', 'run_as__username', 'is_finished'
|
||||
'account', 'is_finished'
|
||||
]
|
||||
search_fields = [
|
||||
'command', 'user__name', 'user__username',
|
||||
'run_as__name', 'run_as__username',
|
||||
'account__username',
|
||||
]
|
||||
ordering = ['-date_created']
|
||||
|
||||
|
@ -121,7 +121,7 @@ class CommandExecutionViewSet(ListModelMixin, OrgGenericViewSet):
|
|||
return queryset.model.objects.none()
|
||||
if current_org.is_root():
|
||||
return queryset
|
||||
queryset = queryset.filter(run_as__org_id=current_org.org_id())
|
||||
# queryset = queryset.filter(run_as__org_id=current_org.org_id())
|
||||
return queryset
|
||||
|
||||
|
||||
|
@ -131,7 +131,7 @@ class CommandExecutionHostRelationViewSet(OrgRelationMixin, OrgBulkModelViewSet)
|
|||
filterset_fields = [
|
||||
'id', 'asset', 'commandexecution'
|
||||
]
|
||||
search_fields = ('asset__hostname', )
|
||||
search_fields = ('asset__name', )
|
||||
http_method_names = ['options', 'get']
|
||||
rbac_perms = {
|
||||
'GET': 'ops.view_commandexecution',
|
||||
|
@ -142,7 +142,7 @@ class CommandExecutionHostRelationViewSet(OrgRelationMixin, OrgBulkModelViewSet)
|
|||
queryset = super().get_queryset()
|
||||
queryset = queryset.annotate(
|
||||
asset_display=Concat(
|
||||
F('asset__hostname'), Value('('),
|
||||
F('asset__name'), Value('('),
|
||||
F('asset__ip'), Value(')')
|
||||
)
|
||||
)
|
||||
|
|
|
@ -88,24 +88,22 @@ class CommandExecutionSerializer(serializers.ModelSerializer):
|
|||
model = CommandExecution
|
||||
fields_mini = ['id']
|
||||
fields_small = fields_mini + [
|
||||
'run_as', 'command', 'is_finished', 'user',
|
||||
'command', 'is_finished', 'user',
|
||||
'date_start', 'result', 'is_success', 'org_id'
|
||||
]
|
||||
fields = fields_small + ['hosts', 'hosts_display', 'run_as_display', 'user_display']
|
||||
fields = fields_small + ['hosts', 'hosts_display', 'user_display']
|
||||
extra_kwargs = {
|
||||
'result': {'label': _('Result')}, # model 上的方法,只能在这修改
|
||||
'is_success': {'label': _('Is success')},
|
||||
'hosts': {'label': _('Hosts')}, # 外键,会生成 sql。不在 model 上修改
|
||||
'run_as': {'label': _('Run as')},
|
||||
'user': {'label': _('User')},
|
||||
'run_as_display': {'label': _('Run as display')},
|
||||
'user_display': {'label': _('User display')},
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
""" Perform necessary eager loading of data. """
|
||||
queryset = queryset.prefetch_related('user', 'run_as', 'hosts')
|
||||
queryset = queryset.prefetch_related('user', 'hosts')
|
||||
return queryset
|
||||
|
||||
|
||||
|
|
|
@ -45,18 +45,12 @@ class ConnectionTokenMixin:
|
|||
@staticmethod
|
||||
def check_user_has_resource_permission(user, asset, application, system_user):
|
||||
from perms.utils.asset import has_asset_system_permission
|
||||
from perms.utils.application import has_application_system_permission
|
||||
|
||||
if asset and not has_asset_system_permission(user, asset, system_user):
|
||||
error = f'User not has this asset and system user permission: ' \
|
||||
f'user={user.id} system_user={system_user.id} asset={asset.id}'
|
||||
raise PermissionDenied(error)
|
||||
|
||||
if application and not has_application_system_permission(user, application, system_user):
|
||||
error = f'User not has this application and system user permission: ' \
|
||||
f'user={user.id} system_user={system_user.id} application={application.id}'
|
||||
raise PermissionDenied(error)
|
||||
|
||||
def get_smart_endpoint(self, protocol, asset=None, application=None):
|
||||
if asset:
|
||||
target_ip = asset.get_target_ip()
|
||||
|
@ -204,8 +198,7 @@ class ConnectionTokenMixin:
|
|||
|
||||
class ConnectionTokenViewSet(ConnectionTokenMixin, RootOrgViewMixin, JMSModelViewSet):
|
||||
filterset_fields = (
|
||||
'type',
|
||||
'user_display', 'system_user_display', 'application_display', 'asset_display'
|
||||
'type', 'user_display', 'asset_display'
|
||||
)
|
||||
search_fields = filterset_fields
|
||||
serializer_classes = {
|
||||
|
|
|
@ -10,7 +10,7 @@ def migrate_system_user_to_account(apps, schema_editor):
|
|||
|
||||
while True:
|
||||
connection_tokens = connection_token_model.objects \
|
||||
.prefetch_related('system_users')[count:bulk_size]
|
||||
.prefetch_related('system_user')[count:bulk_size]
|
||||
if not connection_tokens:
|
||||
break
|
||||
count += len(connection_tokens)
|
||||
|
|
|
@ -85,6 +85,14 @@ class BitOperationChoice:
|
|||
return [(cls.NAME_MAP[i], j) for i, j in cls.DB_CHOICES]
|
||||
|
||||
|
||||
class ChoicesMixin:
|
||||
_value2label_map_: dict
|
||||
|
||||
@classmethod
|
||||
def get_label(cls, value: (str, int)):
|
||||
return cls._value2label_map_[value]
|
||||
|
||||
|
||||
class BaseCreateUpdateModel(models.Model):
|
||||
created_by = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by'))
|
||||
updated_by = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Updated by'))
|
||||
|
|
|
@ -25,7 +25,6 @@ class CommandExecutionViewSet(RootOrgViewMixin, viewsets.ModelViewSet):
|
|||
def check_hosts(self, serializer):
|
||||
data = serializer.validated_data
|
||||
assets = data["hosts"]
|
||||
system_user = data["run_as"]
|
||||
user = self.request.user
|
||||
|
||||
# TOdo:
|
||||
|
|
|
@ -47,10 +47,6 @@ class CommandExecution(OrgModelMixin):
|
|||
inv = JMSInventory(self.allow_assets, run_as=username, system_user=self.run_as)
|
||||
return inv
|
||||
|
||||
@lazyproperty
|
||||
def run_as_display(self):
|
||||
return str(self.run_as)
|
||||
|
||||
@lazyproperty
|
||||
def user_display(self):
|
||||
return str(self.user)
|
||||
|
|
|
@ -138,9 +138,8 @@ class CommandExecutionSerializer(serializers.ModelSerializer):
|
|||
'command', 'result', 'log_url',
|
||||
'is_finished', 'date_created', 'date_finished'
|
||||
]
|
||||
fields_fk = ['run_as']
|
||||
fields_m2m = ['hosts']
|
||||
fields = fields_small + fields_fk + fields_m2m
|
||||
fields = fields_small + fields_m2m
|
||||
read_only_fields = [
|
||||
'result', 'is_finished', 'log_url', 'date_created',
|
||||
'date_finished'
|
||||
|
|
|
@ -80,12 +80,12 @@ class AssetPermissionAssetRelationViewSet(RelationMixin):
|
|||
filterset_fields = [
|
||||
'id', 'asset', 'assetpermission',
|
||||
]
|
||||
search_fields = ["id", "asset__hostname", "asset__ip", "assetpermission__name"]
|
||||
search_fields = ["id", "asset__name", "asset__ip", "assetpermission__name"]
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset \
|
||||
.annotate(asset_display=F('asset__hostname'))
|
||||
.annotate(asset_display=F('asset__name'))
|
||||
return queryset
|
||||
|
||||
|
||||
|
|
|
@ -12,7 +12,9 @@ from rest_framework.generics import (
|
|||
)
|
||||
|
||||
from orgs.utils import tmp_to_root_org
|
||||
from perms.utils.permission import get_asset_system_user_ids_with_actions_by_user, validate_permission
|
||||
from perms.utils.permission import (
|
||||
get_asset_system_user_ids_with_actions_by_user, validate_permission
|
||||
)
|
||||
from common.permissions import IsValidUser
|
||||
from common.utils import get_logger, lazyproperty
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ class AssetGrantedSerializer(serializers.ModelSerializer):
|
|||
class Meta:
|
||||
model = Asset
|
||||
only_fields = [
|
||||
"id", "name", "ip", "protocols", "os", 'domain',
|
||||
"id", "name", "ip", "protocols", 'domain',
|
||||
"platform", "comment", "org_id", "is_active"
|
||||
]
|
||||
fields = only_fields + ['org_name']
|
||||
|
|
Loading…
Reference in New Issue