Merge pull request #4982 from jumpserver/dev

Dev
pull/5041/head
老广 2020-11-12 14:12:35 +08:00 committed by GitHub
commit 841f707b6d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
147 changed files with 3346 additions and 775 deletions

View File

@ -2,8 +2,9 @@
name: 需求建议 name: 需求建议
about: 提出针对本项目的想法和建议 about: 提出针对本项目的想法和建议
title: "[Feature] " title: "[Feature] "
labels: 待处理, 需求 labels: 类型:需求
assignees: 'ibuler' assignees: ibuler
--- ---
**请描述您的需求或者改进建议.** **请描述您的需求或者改进建议.**

View File

@ -2,7 +2,7 @@
name: Bug 提交 name: Bug 提交
about: 提交产品缺陷帮助我们更好的改进 about: 提交产品缺陷帮助我们更好的改进
title: "[Bug] " title: "[Bug] "
labels: bug, 待处理 labels: 类型:bug
assignees: wojiushixiaobai assignees: wojiushixiaobai
--- ---

View File

@ -2,7 +2,7 @@
name: 问题咨询 name: 问题咨询
about: 提出针对本项目安装部署、使用及其他方面的相关问题 about: 提出针对本项目安装部署、使用及其他方面的相关问题
title: "[Question] " title: "[Question] "
labels: 提问, 待处理 labels: 类型:提问
assignees: wojiushixiaobai assignees: wojiushixiaobai
--- ---

View File

@ -1,3 +1,5 @@
from .application import *
from .mixin import *
from .remote_app import * from .remote_app import *
from .database_app import * from .database_app import *
from .k8s_app import * from .k8s_app import *

View File

@ -0,0 +1,20 @@
# coding: utf-8
#
from orgs.mixins.api import OrgBulkModelViewSet
from .mixin import ApplicationAttrsSerializerViewMixin
from ..hands import IsOrgAdminOrAppUser
from .. import models, serializers
__all__ = [
'ApplicationViewSet',
]
class ApplicationViewSet(ApplicationAttrsSerializerViewMixin, OrgBulkModelViewSet):
model = models.Application
filter_fields = ('name', 'type', 'category')
search_fields = filter_fields
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.ApplicationSerializer

View File

@ -0,0 +1,95 @@
from common.exceptions import JMSException
from .. import models
class ApplicationAttrsSerializerViewMixin:
def get_serializer_class(self):
serializer_class = super().get_serializer_class()
app_type = self.request.query_params.get('type')
app_category = self.request.query_params.get('category')
type_options = list(dict(models.Category.get_all_type_serializer_mapper()).keys())
category_options = list(dict(models.Category.get_category_serializer_mapper()).keys())
# ListAPIView 没有 action 属性
# 不使用method属性因为options请求时为method为post
action = getattr(self, 'action', 'list')
if app_type and app_type not in type_options:
raise JMSException(
'Invalid query parameter `type`, select from the following options: {}'
''.format(type_options)
)
if app_category and app_category not in category_options:
raise JMSException(
'Invalid query parameter `category`, select from the following options: {}'
''.format(category_options)
)
if action in [
'create', 'update', 'partial_update', 'bulk_update', 'partial_bulk_update'
] and not app_type:
# action: create / update
raise JMSException(
'The `{}` action must take the `type` query parameter'.format(action)
)
if app_type:
# action: create / update / list / retrieve / metadata
attrs_cls = models.Category.get_type_serializer_cls(app_type)
elif app_category:
# action: list / retrieve / metadata
attrs_cls = models.Category.get_category_serializer_cls(app_category)
else:
attrs_cls = models.Category.get_no_password_serializer_cls()
return type('ApplicationDynamicSerializer', (serializer_class,), {'attrs': attrs_cls()})
class SerializeApplicationToTreeNodeMixin:
@staticmethod
def _serialize_db(db):
return {
'id': db.id,
'name': db.name,
'title': db.name,
'pId': '',
'open': False,
'iconSkin': 'database',
'meta': {'type': 'database_app'}
}
@staticmethod
def _serialize_remote_app(remote_app):
return {
'id': remote_app.id,
'name': remote_app.name,
'title': remote_app.name,
'pId': '',
'open': False,
'isParent': False,
'iconSkin': 'chrome',
'meta': {'type': 'remote_app'}
}
@staticmethod
def _serialize_cloud(cloud):
return {
'id': cloud.id,
'name': cloud.name,
'title': cloud.name,
'pId': '',
'open': False,
'isParent': False,
'iconSkin': 'k8s',
'meta': {'type': 'k8s_app'}
}
def _serialize(self, application):
method_name = f'_serialize_{application.category}'
data = getattr(self, method_name)(application)
return data
def serialize_applications(self, applications):
data = [self._serialize(application) for application in applications]
return data

View File

@ -3,8 +3,9 @@
from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins.api import OrgBulkModelViewSet
from orgs.mixins import generics from orgs.mixins import generics
from common.exceptions import JMSException
from ..hands import IsOrgAdmin, IsAppUser from ..hands import IsOrgAdmin, IsAppUser
from ..models import RemoteApp from .. import models
from ..serializers import RemoteAppSerializer, RemoteAppConnectionInfoSerializer from ..serializers import RemoteAppSerializer, RemoteAppConnectionInfoSerializer
@ -14,7 +15,7 @@ __all__ = [
class RemoteAppViewSet(OrgBulkModelViewSet): class RemoteAppViewSet(OrgBulkModelViewSet):
model = RemoteApp model = models.RemoteApp
filter_fields = ('name', 'type', 'comment') filter_fields = ('name', 'type', 'comment')
search_fields = filter_fields search_fields = filter_fields
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
@ -22,6 +23,18 @@ class RemoteAppViewSet(OrgBulkModelViewSet):
class RemoteAppConnectionInfoApi(generics.RetrieveAPIView): class RemoteAppConnectionInfoApi(generics.RetrieveAPIView):
model = RemoteApp model = models.Application
permission_classes = (IsAppUser, ) permission_classes = (IsAppUser, )
serializer_class = RemoteAppConnectionInfoSerializer serializer_class = RemoteAppConnectionInfoSerializer
@staticmethod
def check_category_allowed(obj):
if not obj.category_is_remote_app:
raise JMSException(
'The request instance(`{}`) is not of category `remote_app`'.format(obj.category)
)
def get_object(self):
obj = super().get_object()
self.check_category_allowed(obj)
return obj

View File

@ -0,0 +1,140 @@
# Generated by Django 2.2.13 on 2020-10-19 12:01
from django.db import migrations, models
import django.db.models.deletion
import django_mysql.models
import uuid
CATEGORY_DB_LIST = ['mysql', 'oracle', 'postgresql', 'mariadb']
CATEGORY_REMOTE_LIST = ['chrome', 'mysql_workbench', 'vmware_client', 'custom']
CATEGORY_CLOUD_LIST = ['k8s']
CATEGORY_DB = 'db'
CATEGORY_REMOTE = 'remote_app'
CATEGORY_CLOUD = 'cloud'
CATEGORY_LIST = [CATEGORY_DB, CATEGORY_REMOTE, CATEGORY_CLOUD]
def get_application_category(old_app):
_type = old_app.type
if _type in CATEGORY_DB_LIST:
category = CATEGORY_DB
elif _type in CATEGORY_REMOTE_LIST:
category = CATEGORY_REMOTE
elif _type in CATEGORY_CLOUD_LIST:
category = CATEGORY_CLOUD
else:
category = None
return category
def common_to_application_json(old_app):
category = get_application_category(old_app)
date_updated = old_app.date_updated if hasattr(old_app, 'date_updated') else old_app.date_created
return {
'id': old_app.id,
'name': old_app.name,
'type': old_app.type,
'category': category,
'comment': old_app.comment,
'created_by': old_app.created_by,
'date_created': old_app.date_created,
'date_updated': date_updated,
'org_id': old_app.org_id
}
def db_to_application_json(database):
app_json = common_to_application_json(database)
app_json.update({
'attrs': {
'host': database.host,
'port': database.port,
'database': database.database
}
})
return app_json
def remote_to_application_json(remote):
app_json = common_to_application_json(remote)
attrs = {
'asset': str(remote.asset.id),
'path': remote.path,
}
attrs.update(remote.params)
app_json.update({
'attrs': attrs
})
return app_json
def k8s_to_application_json(k8s):
app_json = common_to_application_json(k8s)
app_json.update({
'attrs': {
'cluster': k8s.cluster
}
})
return app_json
def migrate_and_integrate_applications(apps, schema_editor):
db_alias = schema_editor.connection.alias
database_app_model = apps.get_model("applications", "DatabaseApp")
remote_app_model = apps.get_model("applications", "RemoteApp")
k8s_app_model = apps.get_model("applications", "K8sApp")
database_apps = database_app_model.objects.using(db_alias).all()
remote_apps = remote_app_model.objects.using(db_alias).all()
k8s_apps = k8s_app_model.objects.using(db_alias).all()
database_applications = [db_to_application_json(db_app) for db_app in database_apps]
remote_applications = [remote_to_application_json(remote_app) for remote_app in remote_apps]
k8s_applications = [k8s_to_application_json(k8s_app) for k8s_app in k8s_apps]
applications_json = database_applications + remote_applications + k8s_applications
application_model = apps.get_model("applications", "Application")
applications = [
application_model(**application_json)
for application_json in applications_json
if application_json['category'] in CATEGORY_LIST
]
for application in applications:
if application_model.objects.using(db_alias).filter(name=application.name).exists():
application.name = '{}-{}'.format(application.name, application.type)
application.save()
class Migration(migrations.Migration):
dependencies = [
('assets', '0057_fill_node_value_assets_amount_and_parent_key'),
('applications', '0005_k8sapp'),
]
operations = [
migrations.CreateModel(
name='Application',
fields=[
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('name', models.CharField(max_length=128, verbose_name='Name')),
('category', models.CharField(choices=[('db', 'Database'), ('remote_app', 'Remote app'), ('cloud', 'Cloud')], max_length=16, verbose_name='Category')),
('type', models.CharField(choices=[('mysql', 'MySQL'), ('oracle', 'Oracle'), ('postgresql', 'PostgreSQL'), ('mariadb', 'MariaDB'), ('chrome', 'Chrome'), ('mysql_workbench', 'MySQL Workbench'), ('vmware_client', 'vSphere Client'), ('custom', 'Custom'), ('k8s', 'Kubernetes')], max_length=16, verbose_name='Type')),
('attrs', django_mysql.models.JSONField(default=dict)),
('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')),
('domain', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='applications', to='assets.Domain', verbose_name='Domain')),
],
options={
'ordering': ('name',),
'unique_together': {('org_id', 'name')},
},
),
migrations.RunPython(migrate_and_integrate_applications),
]

View File

@ -1,3 +1,4 @@
from .application import *
from .remote_app import * from .remote_app import *
from .database_app import * from .database_app import *
from .k8s_app import * from .k8s_app import *

View File

@ -0,0 +1,141 @@
from itertools import chain
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django_mysql.models import JSONField, QuerySet
from orgs.mixins.models import OrgModelMixin
from common.mixins import CommonModelMixin
from common.db.models import ChoiceSet
class DBType(ChoiceSet):
mysql = 'mysql', 'MySQL'
oracle = 'oracle', 'Oracle'
pgsql = 'postgresql', 'PostgreSQL'
mariadb = 'mariadb', 'MariaDB'
@classmethod
def get_type_serializer_cls_mapper(cls):
from ..serializers import database_app
mapper = {
cls.mysql: database_app.MySQLAttrsSerializer,
cls.oracle: database_app.OracleAttrsSerializer,
cls.pgsql: database_app.PostgreAttrsSerializer,
cls.mariadb: database_app.MariaDBAttrsSerializer,
}
return mapper
class RemoteAppType(ChoiceSet):
chrome = 'chrome', 'Chrome'
mysql_workbench = 'mysql_workbench', 'MySQL Workbench'
vmware_client = 'vmware_client', 'vSphere Client'
custom = 'custom', _('Custom')
@classmethod
def get_type_serializer_cls_mapper(cls):
from ..serializers import remote_app
mapper = {
cls.chrome: remote_app.ChromeAttrsSerializer,
cls.mysql_workbench: remote_app.MySQLWorkbenchAttrsSerializer,
cls.vmware_client: remote_app.VMwareClientAttrsSerializer,
cls.custom: remote_app.CustomRemoteAppAttrsSeralizers,
}
return mapper
class CloudType(ChoiceSet):
k8s = 'k8s', 'Kubernetes'
@classmethod
def get_type_serializer_cls_mapper(cls):
from ..serializers import k8s_app
mapper = {
cls.k8s: k8s_app.K8sAttrsSerializer,
}
return mapper
class Category(ChoiceSet):
db = 'db', _('Database')
remote_app = 'remote_app', _('Remote app')
cloud = 'cloud', 'Cloud'
@classmethod
def get_category_type_mapper(cls):
return {
cls.db: DBType,
cls.remote_app: RemoteAppType,
cls.cloud: CloudType
}
@classmethod
def get_category_type_choices_mapper(cls):
return {
name: tp.choices
for name, tp in cls.get_category_type_mapper().items()
}
@classmethod
def get_type_choices(cls, category):
return cls.get_category_type_choices_mapper().get(category, [])
@classmethod
def get_all_type_choices(cls):
all_grouped_choices = tuple(cls.get_category_type_choices_mapper().values())
return tuple(chain(*all_grouped_choices))
@classmethod
def get_all_type_serializer_mapper(cls):
mapper = {}
for tp in cls.get_category_type_mapper().values():
mapper.update(tp.get_type_serializer_cls_mapper())
return mapper
@classmethod
def get_type_serializer_cls(cls, tp):
mapper = cls.get_all_type_serializer_mapper()
return mapper.get(tp, None)
@classmethod
def get_category_serializer_mapper(cls):
from ..serializers import remote_app, database_app, k8s_app
return {
cls.db: database_app.DBAttrsSerializer,
cls.remote_app: remote_app.RemoteAppAttrsSerializer,
cls.cloud: k8s_app.CloudAttrsSerializer,
}
@classmethod
def get_category_serializer_cls(cls, cg):
mapper = cls.get_category_serializer_mapper()
return mapper.get(cg, None)
@classmethod
def get_no_password_serializer_cls(cls):
from ..serializers import common
return common.NoPasswordSerializer
class Application(CommonModelMixin, OrgModelMixin):
name = models.CharField(max_length=128, verbose_name=_('Name'))
domain = models.ForeignKey('assets.Domain', null=True, blank=True, related_name='applications', verbose_name=_("Domain"), on_delete=models.SET_NULL)
category = models.CharField(max_length=16, choices=Category.choices, verbose_name=_('Category'))
type = models.CharField(max_length=16, choices=Category.get_all_type_choices(), verbose_name=_('Type'))
attrs = JSONField()
comment = models.TextField(
max_length=128, default='', blank=True, verbose_name=_('Comment')
)
class Meta:
unique_together = [('org_id', 'name')]
ordering = ('name',)
def __str__(self):
category_display = self.get_category_display()
type_display = self.get_type_display()
return f'{self.name}({type_display})[{category_display}]'
def category_is_remote_app(self):
return self.category == Category.remote_app

View File

@ -1,3 +1,5 @@
from .application import *
from .remote_app import * from .remote_app import *
from .database_app import * from .database_app import *
from .k8s_app import * from .k8s_app import *
from .common import *

View File

@ -0,0 +1,44 @@
# coding: utf-8
#
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from .. import models
__all__ = [
'ApplicationSerializer',
]
class ApplicationSerializer(BulkOrgResourceModelSerializer):
category_display = serializers.ReadOnlyField(source='get_category_display', label=_('Category'))
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type'))
class Meta:
model = models.Application
fields = [
'id', 'name', 'category', 'category_display', 'type', 'type_display', 'attrs',
'domain', 'created_by', 'date_created', 'date_updated', 'comment'
]
read_only_fields = [
'created_by', 'date_created', 'date_updated', 'get_type_display',
]
def create(self, validated_data):
attrs = validated_data.pop('attrs', {})
instance = super().create(validated_data)
instance.attrs = attrs
instance.save()
return instance
def update(self, instance, validated_data):
new_attrs = validated_data.pop('attrs', {})
instance = super().update(instance, validated_data)
attrs = instance.attrs
attrs.update(new_attrs)
instance.attrs = attrs
instance.save()
return instance

View File

@ -0,0 +1,11 @@
from rest_framework import serializers
class NoPasswordSerializer(serializers.JSONField):
def to_representation(self, value):
new_value = {}
for k, v in value.items():
if 'password' not in k:
new_value[k] = v
return new_value

View File

@ -1,14 +1,36 @@
# coding: utf-8 # coding: utf-8
# #
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from common.serializers import AdaptedBulkListSerializer from common.serializers import AdaptedBulkListSerializer
from .. import models from .. import models
__all__ = [
'DatabaseAppSerializer', class DBAttrsSerializer(serializers.Serializer):
] host = serializers.CharField(max_length=128, label=_('Host'))
port = serializers.IntegerField(label=_('Port'))
database = serializers.CharField(
max_length=128, required=False, allow_blank=True, allow_null=True, label=_('Database')
)
class MySQLAttrsSerializer(DBAttrsSerializer):
port = serializers.IntegerField(default=3306, label=_('Port'))
class PostgreAttrsSerializer(DBAttrsSerializer):
port = serializers.IntegerField(default=5432, label=_('Port'))
class OracleAttrsSerializer(DBAttrsSerializer):
port = serializers.IntegerField(default=1521, label=_('Port'))
class MariaDBAttrsSerializer(MySQLAttrsSerializer):
pass
class DatabaseAppSerializer(BulkOrgResourceModelSerializer): class DatabaseAppSerializer(BulkOrgResourceModelSerializer):
@ -24,3 +46,6 @@ class DatabaseAppSerializer(BulkOrgResourceModelSerializer):
'created_by', 'date_created', 'date_updated' 'created_by', 'date_created', 'date_updated'
'get_type_display', 'get_type_display',
] ]
extra_kwargs = {
'get_type_display': {'label': _('Type for display')},
}

View File

@ -1,15 +1,20 @@
from rest_framework import serializers from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from .. import models from .. import models
__all__ = [
'K8sAppSerializer', class CloudAttrsSerializer(serializers.Serializer):
] cluster = serializers.CharField(max_length=1024, label=_('Cluster'))
class K8sAttrsSerializer(CloudAttrsSerializer):
pass
class K8sAppSerializer(BulkOrgResourceModelSerializer): class K8sAppSerializer(BulkOrgResourceModelSerializer):
type_display = serializers.CharField(source='get_type_display', read_only=True) type_display = serializers.CharField(source='get_type_display', read_only=True, label=_('Type for display'))
class Meta: class Meta:
model = models.K8sApp model = models.K8sApp

View File

@ -2,21 +2,138 @@
# #
import copy import copy
from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ObjectDoesNotExist
from rest_framework import serializers from rest_framework import serializers
from common.serializers import AdaptedBulkListSerializer from common.serializers import AdaptedBulkListSerializer
from common.fields.serializer import CustomMetaDictField from common.fields.serializer import CustomMetaDictField
from common.utils import get_logger
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from assets.models import Asset
from .. import const from .. import const
from ..models import RemoteApp from ..models import RemoteApp, Category, Application
logger = get_logger(__file__)
__all__ = [ class CharPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
'RemoteAppSerializer', 'RemoteAppConnectionInfoSerializer',
] def to_internal_value(self, data):
instance = super().to_internal_value(data)
return str(instance.id)
def to_representation(self, value):
# value is instance.id
if self.pk_field is not None:
return self.pk_field.to_representation(value)
return value
class RemoteAppAttrsSerializer(serializers.Serializer):
asset_info = serializers.SerializerMethodField()
asset = CharPrimaryKeyRelatedField(queryset=Asset.objects, required=False, label=_("Asset"))
path = serializers.CharField(max_length=128, label=_('Application path'))
@staticmethod
def get_asset_info(obj):
asset_info = {}
asset_id = obj.get('asset')
if not asset_id:
return asset_info
try:
asset = Asset.objects.get(id=asset_id)
asset_info.update({
'id': str(asset.id),
'hostname': asset.hostname
})
except ObjectDoesNotExist as e:
logger.error(e)
return asset_info
class ChromeAttrsSerializer(RemoteAppAttrsSerializer):
REMOTE_APP_PATH = 'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe'
path = serializers.CharField(max_length=128, label=_('Application path'), default=REMOTE_APP_PATH)
chrome_target = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Target URL'))
chrome_username = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Username'))
chrome_password = serializers.CharField(max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password'))
class MySQLWorkbenchAttrsSerializer(RemoteAppAttrsSerializer):
REMOTE_APP_PATH = 'C:\Program Files\MySQL\MySQL Workbench 8.0 CE\MySQLWorkbench.exe'
path = serializers.CharField(max_length=128, label=_('Application path'), default=REMOTE_APP_PATH)
mysql_workbench_ip = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('IP'))
mysql_workbench_port = serializers.IntegerField(required=False, label=_('Port'))
mysql_workbench_name = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Database'))
mysql_workbench_username = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Username'))
mysql_workbench_password = serializers.CharField(max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password'))
class VMwareClientAttrsSerializer(RemoteAppAttrsSerializer):
REMOTE_APP_PATH = 'C:\Program Files (x86)\VMware\Infrastructure\Virtual Infrastructure Client\Launcher\VpxClient.exe'
path = serializers.CharField(max_length=128, label=_('Application path'), default=REMOTE_APP_PATH)
vmware_target = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Target URL'))
vmware_username = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Username'))
vmware_password = serializers.CharField(max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password'))
class CustomRemoteAppAttrsSeralizers(RemoteAppAttrsSerializer):
custom_cmdline = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Operating parameter'))
custom_target = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Target url'))
custom_username = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Username'))
custom_password = serializers.CharField(max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password'))
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_parameters(obj):
"""
返回Guacamole需要的RemoteApp配置参数信息中的parameters参数
"""
serializer_cls = Category.get_type_serializer_cls(obj.type)
fields = serializer_cls().get_fields()
fields.pop('asset', None)
fields_name = list(fields.keys())
attrs = obj.attrs
_parameters = list()
_parameters.append(obj.type)
for field_name in list(fields_name):
value = attrs.get(field_name, None)
if not value:
continue
if field_name == 'path':
value = '\"%s\"' % value
_parameters.append(str(value))
_parameters = ' '.join(_parameters)
return _parameters
def get_parameter_remote_app(self, obj):
parameters = self.get_parameters(obj)
parameter = {
'program': const.REMOTE_APP_BOOT_PROGRAM_NAME,
'working_directory': '',
'parameters': parameters,
}
return parameter
@staticmethod
def get_asset(obj):
return obj.attrs.get('asset')
# TODO: DELETE
class RemoteAppParamsDictField(CustomMetaDictField): class RemoteAppParamsDictField(CustomMetaDictField):
type_fields_map = const.REMOTE_APP_TYPE_FIELDS_MAP type_fields_map = const.REMOTE_APP_TYPE_FIELDS_MAP
default_type = const.REMOTE_APP_TYPE_CHROME default_type = const.REMOTE_APP_TYPE_CHROME
@ -24,8 +141,9 @@ class RemoteAppParamsDictField(CustomMetaDictField):
convert_key_to_upper = False convert_key_to_upper = False
# TODO: DELETE
class RemoteAppSerializer(BulkOrgResourceModelSerializer): class RemoteAppSerializer(BulkOrgResourceModelSerializer):
params = RemoteAppParamsDictField() params = RemoteAppParamsDictField(label=_('Parameters'))
type_fields_map = const.REMOTE_APP_TYPE_FIELDS_MAP type_fields_map = const.REMOTE_APP_TYPE_FIELDS_MAP
class Meta: class Meta:
@ -39,6 +157,10 @@ class RemoteAppSerializer(BulkOrgResourceModelSerializer):
'created_by', 'date_created', 'asset_info', 'created_by', 'date_created', 'asset_info',
'get_type_display' 'get_type_display'
] ]
extra_kwargs = {
'asset_info': {'label': _('Asset info')},
'get_type_display': {'label': _('Type for display')},
}
def process_params(self, instance, validated_data): def process_params(self, instance, validated_data):
new_params = copy.deepcopy(validated_data.get('params', {})) new_params = copy.deepcopy(validated_data.get('params', {}))
@ -66,21 +188,3 @@ class RemoteAppSerializer(BulkOrgResourceModelSerializer):
return super().update(instance, validated_data) return super().update(instance, validated_data)
class RemoteAppConnectionInfoSerializer(serializers.ModelSerializer):
parameter_remote_app = serializers.SerializerMethodField()
class Meta:
model = RemoteApp
fields = [
'id', 'name', 'asset', 'parameter_remote_app',
]
read_only_fields = ['parameter_remote_app']
@staticmethod
def get_parameter_remote_app(obj):
parameter = {
'program': const.REMOTE_APP_BOOT_PROGRAM_NAME,
'working_directory': '',
'parameters': obj.parameters,
}
return parameter

View File

@ -10,6 +10,7 @@ from .. import api
app_name = 'applications' app_name = 'applications'
router = BulkRouter() router = BulkRouter()
router.register(r'applications', api.ApplicationViewSet, 'application')
router.register(r'remote-apps', api.RemoteAppViewSet, 'remote-app') router.register(r'remote-apps', api.RemoteAppViewSet, 'remote-app')
router.register(r'database-apps', api.DatabaseAppViewSet, 'database-app') router.register(r'database-apps', api.DatabaseAppViewSet, 'database-app')
router.register(r'k8s-apps', api.K8sAppViewSet, 'k8s-app') router.register(r'k8s-apps', api.K8sAppViewSet, 'k8s-app')

View File

@ -94,7 +94,6 @@ class AdminUserAssetsListView(generics.ListAPIView):
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
serializer_class = serializers.AssetSimpleSerializer serializer_class = serializers.AssetSimpleSerializer
filter_fields = ("hostname", "ip") filter_fields = ("hostname", "ip")
http_method_names = ['get']
search_fields = filter_fields search_fields = filter_fields
def get_object(self): def get_object(self):

View File

@ -32,7 +32,7 @@ class AssetViewSet(FilterAssetByNodeMixin, OrgBulkModelViewSet):
model = Asset model = Asset
filter_fields = ( filter_fields = (
"hostname", "ip", "systemuser__id", "admin_user__id", "platform__base", "hostname", "ip", "systemuser__id", "admin_user__id", "platform__base",
"is_active", 'ip' "is_active"
) )
search_fields = ("hostname", "ip") search_fields = ("hostname", "ip")
ordering_fields = ("hostname", "ip", "port", "cpu_cores") ordering_fields = ("hostname", "ip", "port", "cpu_cores")

View File

@ -1,7 +1,9 @@
# ~*~ coding: utf-8 ~*~ # ~*~ coding: utf-8 ~*~
from rest_framework.views import APIView, Response
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from django.utils.translation import ugettext as _
from rest_framework.views import APIView, Response
from rest_framework.serializers import ValidationError
from common.utils import get_logger from common.utils import get_logger
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
@ -42,6 +44,10 @@ class GatewayTestConnectionApi(SingleObjectMixin, APIView):
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
self.object = self.get_object(Gateway.objects.all()) self.object = self.get_object(Gateway.objects.all())
local_port = self.request.data.get('port') or self.object.port local_port = self.request.data.get('port') or self.object.port
try:
local_port = int(local_port)
except ValueError:
raise ValidationError({'port': _('Number required')})
ok, e = self.object.test_connective(local_port=local_port) ok, e = self.object.test_connective(local_port=local_port)
if ok: if ok:
return Response("ok") return Response("ok")

View File

@ -19,7 +19,7 @@ from ..tasks import (
logger = get_logger(__file__) logger = get_logger(__file__)
__all__ = [ __all__ = [
'SystemUserViewSet', 'SystemUserAuthInfoApi', 'SystemUserAssetAuthInfoApi', 'SystemUserViewSet', 'SystemUserAuthInfoApi', 'SystemUserAssetAuthInfoApi',
'SystemUserCommandFilterRuleListApi', 'SystemUserTaskApi', 'SystemUserCommandFilterRuleListApi', 'SystemUserTaskApi', 'SystemUserAssetsListView',
] ]
@ -125,3 +125,18 @@ class SystemUserCommandFilterRuleListApi(generics.ListAPIView):
pk = self.kwargs.get('pk', None) pk = self.kwargs.get('pk', None)
system_user = get_object_or_404(SystemUser, pk=pk) system_user = get_object_or_404(SystemUser, pk=pk)
return system_user.cmd_filter_rules return system_user.cmd_filter_rules
class SystemUserAssetsListView(generics.ListAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.AssetSimpleSerializer
filter_fields = ("hostname", "ip")
search_fields = filter_fields
def get_object(self):
pk = self.kwargs.get('pk')
return get_object_or_404(SystemUser, pk=pk)
def get_queryset(self):
system_user = self.get_object()
return system_user.get_all_assets()

View File

@ -95,7 +95,7 @@ class SystemUserNodeRelationViewSet(BaseRelationViewSet):
'id', 'node', 'systemuser', 'id', 'node', 'systemuser',
] ]
search_fields = [ search_fields = [
"node__value", "systemuser__name", "systemuser_username" "node__value", "systemuser__name", "systemuser__username"
] ]
def get_objects_attr(self): def get_objects_attr(self):

View File

@ -0,0 +1,27 @@
# Generated by Django 2.2.13 on 2020-10-23 03:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0057_fill_node_value_assets_amount_and_parent_key'),
]
operations = [
migrations.AlterModelOptions(
name='asset',
options={'ordering': ['-date_created'], 'verbose_name': 'Asset'},
),
migrations.AlterField(
model_name='asset',
name='comment',
field=models.TextField(blank=True, default='', verbose_name='Comment'),
),
migrations.AlterField(
model_name='commandfilterrule',
name='content',
field=models.TextField(help_text='One line one command', verbose_name='Content'),
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 2.2.13 on 2020-10-27 11:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0058_auto_20201023_1115'),
]
operations = [
migrations.AlterField(
model_name='systemuser',
name='protocol',
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet'), ('vnc', 'vnc'), ('mysql', 'mysql'), ('oracle', 'oracle'), ('mariadb', 'mariadb'), ('postgresql', 'postgresql'), ('k8s', 'k8s')], default='ssh', max_length=16, verbose_name='Protocol'),
),
migrations.AddField(
model_name='systemuser',
name='ad_domain',
field=models.CharField(default='', max_length=256),
),
migrations.AlterField(
model_name='gateway',
name='ip',
field=models.CharField(db_index=True, max_length=128, verbose_name='IP'),
),
]

View File

@ -0,0 +1,58 @@
# Generated by Django 2.2.13 on 2020-10-26 11:31
from django.db import migrations, models
def get_node_ancestor_keys(key, with_self=False):
parent_keys = []
key_list = key.split(":")
if not with_self:
key_list.pop()
for i in range(len(key_list)):
parent_keys.append(":".join(key_list))
key_list.pop()
return parent_keys
def migrate_nodes_value_with_slash(apps, schema_editor):
model = apps.get_model("assets", "Node")
db_alias = schema_editor.connection.alias
nodes = model.objects.using(db_alias).filter(value__contains='/')
print('')
print("- Start migrate node value if has /")
for i, node in enumerate(list(nodes)):
new_value = node.value.replace('/', '|')
print("{} start migrate node value: {} => {}".format(i, node.value, new_value))
node.value = new_value
node.save()
def migrate_nodes_full_value(apps, schema_editor):
model = apps.get_model("assets", "Node")
db_alias = schema_editor.connection.alias
nodes = model.objects.using(db_alias).all()
print("- Start migrate node full value")
for i, node in enumerate(list(nodes)):
print("{} start migrate {} node full value".format(i, node.value))
ancestor_keys = get_node_ancestor_keys(node.key, True)
values = model.objects.filter(key__in=ancestor_keys).values_list('key', 'value')
values = [v for k, v in sorted(values, key=lambda x: len(x[0]))]
node.full_value = '/' + '/'.join(values)
node.save()
class Migration(migrations.Migration):
dependencies = [
('assets', '0059_auto_20201027_1905'),
]
operations = [
migrations.AddField(
model_name='node',
name='full_value',
field=models.CharField(default='', max_length=4096, verbose_name='Full value'),
),
migrations.RunPython(migrate_nodes_value_with_slash),
migrations.RunPython(migrate_nodes_full_value)
]

View File

@ -227,7 +227,7 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
labels = models.ManyToManyField('assets.Label', blank=True, related_name='assets', verbose_name=_("Labels")) labels = models.ManyToManyField('assets.Label', blank=True, related_name='assets', verbose_name=_("Labels"))
created_by = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Created by')) created_by = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Created by'))
date_created = models.DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created')) date_created = models.DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created'))
comment = models.TextField(max_length=128, default='', blank=True, verbose_name=_('Comment')) comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
objects = AssetManager.from_queryset(AssetQuerySet)() objects = AssetManager.from_queryset(AssetQuerySet)()
org_objects = AssetOrgManager.from_queryset(AssetQuerySet)() org_objects = AssetOrgManager.from_queryset(AssetQuerySet)()
@ -313,6 +313,12 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
} }
return info return info
def nodes_display(self):
names = []
for n in self.nodes.all():
names.append(n.full_value)
return names
def as_node(self): def as_node(self):
from .node import Node from .node import Node
fake_node = Node() fake_node = Node()
@ -355,3 +361,4 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
class Meta: class Meta:
unique_together = [('org_id', 'hostname')] unique_together = [('org_id', 'hostname')]
verbose_name = _("Asset") verbose_name = _("Asset")
ordering = ['-date_created']

View File

@ -52,7 +52,7 @@ class CommandFilterRule(OrgModelMixin):
type = models.CharField(max_length=16, default=TYPE_COMMAND, choices=TYPE_CHOICES, verbose_name=_("Type")) type = models.CharField(max_length=16, default=TYPE_COMMAND, choices=TYPE_CHOICES, verbose_name=_("Type"))
priority = models.IntegerField(default=50, verbose_name=_("Priority"), help_text=_("1-100, the higher will be match first"), priority = models.IntegerField(default=50, verbose_name=_("Priority"), help_text=_("1-100, the higher will be match first"),
validators=[MinValueValidator(1), MaxValueValidator(100)]) validators=[MinValueValidator(1), MaxValueValidator(100)])
content = models.TextField(max_length=1024, verbose_name=_("Content"), help_text=_("One line one command")) content = models.TextField(verbose_name=_("Content"), help_text=_("One line one command"))
action = models.IntegerField(default=ACTION_DENY, choices=ACTION_CHOICES, verbose_name=_("Action")) action = models.IntegerField(default=ACTION_DENY, choices=ACTION_CHOICES, verbose_name=_("Action"))
comment = models.CharField(max_length=64, blank=True, default='', verbose_name=_("Comment")) comment = models.CharField(max_length=64, blank=True, default='', verbose_name=_("Comment"))
date_created = models.DateTimeField(auto_now_add=True) date_created = models.DateTimeField(auto_now_add=True)

View File

@ -9,6 +9,7 @@ import paramiko
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from common.utils.strings import no_special_chars
from orgs.mixins.models import OrgModelMixin from orgs.mixins.models import OrgModelMixin
from .base import BaseUser from .base import BaseUser
@ -47,7 +48,7 @@ class Gateway(BaseUser):
(PROTOCOL_SSH, 'ssh'), (PROTOCOL_SSH, 'ssh'),
(PROTOCOL_RDP, 'rdp'), (PROTOCOL_RDP, 'rdp'),
) )
ip = models.GenericIPAddressField(max_length=32, verbose_name=_('IP'), db_index=True) ip = models.CharField(max_length=128, verbose_name=_('IP'), db_index=True)
port = models.IntegerField(default=22, verbose_name=_('Port')) port = models.IntegerField(default=22, verbose_name=_('Port'))
protocol = models.CharField(choices=PROTOCOL_CHOICES, max_length=16, default=PROTOCOL_SSH, verbose_name=_("Protocol")) protocol = models.CharField(choices=PROTOCOL_CHOICES, max_length=16, default=PROTOCOL_SSH, verbose_name=_("Protocol"))
domain = models.ForeignKey(Domain, on_delete=models.CASCADE, verbose_name=_("Domain")) domain = models.ForeignKey(Domain, on_delete=models.CASCADE, verbose_name=_("Domain"))
@ -64,8 +65,8 @@ class Gateway(BaseUser):
def test_connective(self, local_port=None): def test_connective(self, local_port=None):
if local_port is None: if local_port is None:
local_port = self.port local_port = self.port
if self.password and not re.match(r'\w+$', self.password): if self.password and not no_special_chars(self.password):
return False, _("Password should not contain special characters") return False, _("Password should not contains special characters")
client = paramiko.SSHClient() client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.set_missing_host_key_policy(paramiko.AutoAddPolicy())

View File

@ -22,7 +22,7 @@ class FavoriteAsset(CommonModelMixin):
@classmethod @classmethod
def get_user_favorite_assets(cls, user): def get_user_favorite_assets(cls, user):
from assets.models import Asset from assets.models import Asset
from perms.utils.user_asset_permission import get_user_granted_all_assets from perms.utils.asset.user_permission import get_user_granted_all_assets
asset_ids = get_user_granted_all_assets(user).values_list('id', flat=True) asset_ids = get_user_granted_all_assets(user).values_list('id', flat=True)
query_name = cls.asset.field.related_query_name() query_name = cls.asset.field.related_query_name()
return Asset.org_objects.filter(**{f'{query_name}__user_id': user.id}, id__in=asset_ids).distinct() return Asset.org_objects.filter(**{f'{query_name}__user_id': user.id}, id__in=asset_ids).distinct()

View File

@ -13,7 +13,7 @@ from django.db.transaction import atomic
from common.utils import get_logger from common.utils import get_logger
from common.utils.common import lazyproperty from common.utils.common import lazyproperty
from orgs.mixins.models import OrgModelMixin, OrgManager from orgs.mixins.models import OrgModelMixin, OrgManager
from orgs.utils import get_current_org, tmp_to_org, current_org from orgs.utils import get_current_org, tmp_to_org
from orgs.models import Organization from orgs.models import Organization
@ -205,6 +205,28 @@ class FamilyMixin:
sibling = sibling.exclude(key=self.key) sibling = sibling.exclude(key=self.key)
return sibling return sibling
@classmethod
def create_node_by_full_value(cls, full_value):
if not full_value:
return []
nodes_family = full_value.split('/')
org_root = cls.org_root()
if nodes_family[0] == org_root.value:
nodes_family = nodes_family[1:]
return cls.create_nodes_recurse(nodes_family, org_root)
@classmethod
def create_nodes_recurse(cls, values, parent=None):
if not values:
return None
if parent is None:
parent = cls.org_root()
value = values[0]
child, created = parent.get_or_create_child(value=value)
if len(values) == 1:
return child
return cls.create_nodes_recurse(values[1:], child)
def get_family(self): def get_family(self):
ancestors = self.get_ancestors() ancestors = self.get_ancestors()
children = self.get_all_children() children = self.get_all_children()
@ -372,6 +394,7 @@ class Node(OrgModelMixin, SomeNodesMixin, FamilyMixin, NodeAssetsMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
key = models.CharField(unique=True, max_length=64, verbose_name=_("Key")) # '1:1:1:1' key = models.CharField(unique=True, max_length=64, verbose_name=_("Key")) # '1:1:1:1'
value = models.CharField(max_length=128, verbose_name=_("Value")) value = models.CharField(max_length=128, verbose_name=_("Value"))
full_value = models.CharField(max_length=4096, verbose_name=_('Full value'), default='')
child_mark = models.IntegerField(default=0) child_mark = models.IntegerField(default=0)
date_create = models.DateTimeField(auto_now_add=True) date_create = models.DateTimeField(auto_now_add=True)
parent_key = models.CharField(max_length=64, verbose_name=_("Parent key"), parent_key = models.CharField(max_length=64, verbose_name=_("Parent key"),
@ -387,7 +410,7 @@ class Node(OrgModelMixin, SomeNodesMixin, FamilyMixin, NodeAssetsMixin):
ordering = ['key'] ordering = ['key']
def __str__(self): def __str__(self):
return self.value return self.full_value
# def __eq__(self, other): # def __eq__(self, other):
# if not other: # if not other:
@ -411,15 +434,14 @@ class Node(OrgModelMixin, SomeNodesMixin, FamilyMixin, NodeAssetsMixin):
def name(self): def name(self):
return self.value return self.value
@lazyproperty def computed_full_value(self):
def full_value(self):
# 不要在列表中调用该属性 # 不要在列表中调用该属性
values = self.__class__.objects.filter( values = self.__class__.objects.filter(
key__in=self.get_ancestor_keys() key__in=self.get_ancestor_keys()
).values_list('key', 'value') ).values_list('key', 'value')
values = [v for k, v in sorted(values, key=lambda x: len(x[0]))] values = [v for k, v in sorted(values, key=lambda x: len(x[0]))]
values.append(self.value) values.append(str(self.value))
return ' / '.join(values) return '/' + '/'.join(values)
@property @property
def level(self): def level(self):
@ -458,3 +480,22 @@ class Node(OrgModelMixin, SomeNodesMixin, FamilyMixin, NodeAssetsMixin):
if self.has_children_or_has_assets(): if self.has_children_or_has_assets():
return return
return super().delete(using=using, keep_parents=keep_parents) return super().delete(using=using, keep_parents=keep_parents)
def update_child_full_value(self):
nodes = self.get_all_children(with_self=True)
sort_key_func = lambda n: [int(i) for i in n.key.split(':')]
nodes_sorted = sorted(list(nodes), key=sort_key_func)
nodes_mapper = {n.key: n for n in nodes_sorted}
for node in nodes_sorted:
parent = nodes_mapper.get(node.parent_key)
if not parent:
logger.error(f'Node parent node in mapper: {node.parent_key} {node.value}')
continue
node.full_value = parent.full_value + '/' + node.value
self.__class__.objects.bulk_update(nodes, ['full_value'])
def save(self, *args, **kwargs):
self.full_value = self.computed_full_value()
instance = super().save(*args, **kwargs)
self.update_child_full_value()
return instance

View File

@ -72,6 +72,9 @@ class SystemUser(BaseUser):
PROTOCOL_TELNET = 'telnet' PROTOCOL_TELNET = 'telnet'
PROTOCOL_VNC = 'vnc' PROTOCOL_VNC = 'vnc'
PROTOCOL_MYSQL = 'mysql' PROTOCOL_MYSQL = 'mysql'
PROTOCOL_ORACLE = 'oracle'
PROTOCOL_MARIADB = 'mariadb'
PROTOCOL_POSTGRESQL = 'postgresql'
PROTOCOL_K8S = 'k8s' PROTOCOL_K8S = 'k8s'
PROTOCOL_CHOICES = ( PROTOCOL_CHOICES = (
(PROTOCOL_SSH, 'ssh'), (PROTOCOL_SSH, 'ssh'),
@ -79,6 +82,9 @@ class SystemUser(BaseUser):
(PROTOCOL_TELNET, 'telnet'), (PROTOCOL_TELNET, 'telnet'),
(PROTOCOL_VNC, 'vnc'), (PROTOCOL_VNC, 'vnc'),
(PROTOCOL_MYSQL, 'mysql'), (PROTOCOL_MYSQL, 'mysql'),
(PROTOCOL_ORACLE, 'oracle'),
(PROTOCOL_MARIADB, 'mariadb'),
(PROTOCOL_POSTGRESQL, 'postgresql'),
(PROTOCOL_K8S, 'k8s'), (PROTOCOL_K8S, 'k8s'),
) )
@ -104,6 +110,7 @@ class SystemUser(BaseUser):
token = models.TextField(default='', verbose_name=_('Token')) token = models.TextField(default='', verbose_name=_('Token'))
home = models.CharField(max_length=4096, default='', verbose_name=_('Home'), blank=True) home = models.CharField(max_length=4096, default='', verbose_name=_('Home'), blank=True)
system_groups = models.CharField(default='', max_length=4096, verbose_name=_('System groups'), blank=True) system_groups = models.CharField(default='', max_length=4096, verbose_name=_('System groups'), blank=True)
ad_domain = models.CharField(default='', max_length=256)
_prefer = 'system_user' _prefer = 'system_user'
def __str__(self): def __str__(self):
@ -126,6 +133,24 @@ class SystemUser(BaseUser):
def login_mode_display(self): def login_mode_display(self):
return self.get_login_mode_display() return self.get_login_mode_display()
@property
def db_application_protocols(self):
return [
self.PROTOCOL_MYSQL, self.PROTOCOL_ORACLE, self.PROTOCOL_MARIADB,
self.PROTOCOL_POSTGRESQL
]
@property
def cloud_application_protocols(self):
return [self.PROTOCOL_K8S]
@property
def application_category_protocols(self):
protocols = []
protocols.extend(self.db_application_protocols)
protocols.extend(self.cloud_application_protocols)
return protocols
def is_need_push(self): def is_need_push(self):
if self.auto_push and self.protocol in [self.PROTOCOL_SSH, self.PROTOCOL_RDP]: if self.auto_push and self.protocol in [self.PROTOCOL_SSH, self.PROTOCOL_RDP]:
return True return True
@ -138,11 +163,11 @@ class SystemUser(BaseUser):
@property @property
def is_need_test_asset_connective(self): def is_need_test_asset_connective(self):
return self.protocol not in [self.PROTOCOL_MYSQL] return self.protocol not in self.application_category_protocols
@property @property
def can_perm_to_asset(self): def can_perm_to_asset(self):
return self.protocol not in [self.PROTOCOL_MYSQL] return self.protocol not in self.application_category_protocols
def _merge_auth(self, other): def _merge_auth(self, other):
super()._merge_auth(other) super()._merge_auth(other)

View File

@ -1,13 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from rest_framework import serializers from rest_framework import serializers
from django.db.models import Prefetch, F, Count from django.db.models import F
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from common.serializers import AdaptedBulkListSerializer from ..models import Asset, Node, Platform
from ..models import Asset, Node, Label, Platform
from .base import ConnectivitySerializer from .base import ConnectivitySerializer
__all__ = [ __all__ = [
@ -67,8 +66,9 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
slug_field='name', queryset=Platform.objects.all(), label=_("Platform") slug_field='name', queryset=Platform.objects.all(), label=_("Platform")
) )
protocols = ProtocolsField(label=_('Protocols'), required=False) protocols = ProtocolsField(label=_('Protocols'), required=False)
domain_display = serializers.ReadOnlyField(source='domain.name') domain_display = serializers.ReadOnlyField(source='domain.name', label=_('Domain name'))
admin_user_display = serializers.ReadOnlyField(source='admin_user.name') admin_user_display = serializers.ReadOnlyField(source='admin_user.name', label=_('Admin user name'))
nodes_display = serializers.ListField(child=serializers.CharField(), label=_('Nodes name'), required=False)
""" """
资产的数据结构 资产的数据结构
@ -90,7 +90,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
'platform': ['name'] 'platform': ['name']
} }
fields_m2m = [ fields_m2m = [
'nodes', 'labels', 'nodes', 'nodes_display', 'labels',
] ]
annotates_fields = { annotates_fields = {
# 'admin_user_display': 'admin_user__name' # 'admin_user_display': 'admin_user__name'
@ -133,14 +133,32 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
if protocols_data: if protocols_data:
validated_data["protocols"] = ' '.join(protocols_data) validated_data["protocols"] = ' '.join(protocols_data)
def perform_nodes_display_create(self, instance, nodes_display):
if not nodes_display:
return
nodes_to_set = []
for full_value in nodes_display:
node = Node.objects.filter(full_value=full_value).first()
if node:
nodes_to_set.append(node)
else:
node = Node.create_node_by_full_value(full_value)
nodes_to_set.append(node)
instance.nodes.set(nodes_to_set)
def create(self, validated_data): def create(self, validated_data):
self.compatible_with_old_protocol(validated_data) self.compatible_with_old_protocol(validated_data)
nodes_display = validated_data.pop('nodes_display', '')
instance = super().create(validated_data) instance = super().create(validated_data)
self.perform_nodes_display_create(instance, nodes_display)
return instance return instance
def update(self, instance, validated_data): def update(self, instance, validated_data):
nodes_display = validated_data.pop('nodes_display', '')
self.compatible_with_old_protocol(validated_data) self.compatible_with_old_protocol(validated_data)
return super().update(instance, validated_data) instance = super().update(instance, validated_data)
self.perform_nodes_display_create(instance, nodes_display)
return instance
class AssetDisplaySerializer(AssetSerializer): class AssetDisplaySerializer(AssetSerializer):

View File

@ -1,17 +1,19 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from rest_framework import serializers from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from common.serializers import AdaptedBulkListSerializer from common.serializers import AdaptedBulkListSerializer
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from common.validators import NoSpecialChars
from ..models import Domain, Gateway from ..models import Domain, Gateway
from .base import AuthSerializerMixin from .base import AuthSerializerMixin
class DomainSerializer(BulkOrgResourceModelSerializer): class DomainSerializer(BulkOrgResourceModelSerializer):
asset_count = serializers.SerializerMethodField() asset_count = serializers.SerializerMethodField(label=_('Assets count'))
gateway_count = serializers.SerializerMethodField() application_count = serializers.SerializerMethodField(label=_('Applications count'))
gateway_count = serializers.SerializerMethodField(label=_('Gateways count'))
class Meta: class Meta:
model = Domain model = Domain
@ -20,12 +22,12 @@ class DomainSerializer(BulkOrgResourceModelSerializer):
'comment', 'date_created' 'comment', 'date_created'
] ]
fields_m2m = [ fields_m2m = [
'asset_count', 'assets', 'gateway_count', 'asset_count', 'assets', 'application_count', 'gateway_count',
] ]
fields = fields_small + fields_m2m fields = fields_small + fields_m2m
read_only_fields = ('asset_count', 'gateway_count', 'date_created') read_only_fields = ('asset_count', 'gateway_count', 'date_created')
extra_kwargs = { extra_kwargs = {
'assets': {'required': False} 'assets': {'required': False, 'label': _('Assets')},
} }
list_serializer_class = AdaptedBulkListSerializer list_serializer_class = AdaptedBulkListSerializer
@ -33,6 +35,10 @@ class DomainSerializer(BulkOrgResourceModelSerializer):
def get_asset_count(obj): def get_asset_count(obj):
return obj.assets.count() return obj.assets.count()
@staticmethod
def get_application_count(obj):
return obj.applications.count()
@staticmethod @staticmethod
def get_gateway_count(obj): def get_gateway_count(obj):
return obj.gateway_set.all().count() return obj.gateway_set.all().count()
@ -47,6 +53,9 @@ class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
'private_key', 'public_key', 'domain', 'is_active', 'date_created', 'private_key', 'public_key', 'domain', 'is_active', 'date_created',
'date_updated', 'created_by', 'comment', 'date_updated', 'created_by', 'comment',
] ]
extra_kwargs = {
'password': {'validators': [NoSpecialChars()]}
}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)

View File

@ -25,6 +25,9 @@ class NodeSerializer(BulkOrgResourceModelSerializer):
read_only_fields = ['key', 'org_id'] read_only_fields = ['key', 'org_id']
def validate_value(self, data): def validate_value(self, data):
if '/' in data:
error = _("Can't contains: " + "/")
raise serializers.ValidationError(error)
if self.instance: if self.instance:
instance = self.instance instance = self.instance
siblings = instance.get_siblings() siblings = instance.get_siblings()

View File

@ -6,7 +6,6 @@ from common.serializers import AdaptedBulkListSerializer
from common.mixins.serializers import BulkSerializerMixin from common.mixins.serializers import BulkSerializerMixin
from common.utils import ssh_pubkey_gen from common.utils import ssh_pubkey_gen
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from assets.models import Node
from ..models import SystemUser, Asset from ..models import SystemUser, Asset
from .base import AuthSerializerMixin from .base import AuthSerializerMixin
@ -35,17 +34,18 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
'auto_push', 'cmd_filters', 'sudo', 'shell', 'comment', 'auto_push', 'cmd_filters', 'sudo', 'shell', 'comment',
'auto_generate_key', 'sftp_root', 'token', 'auto_generate_key', 'sftp_root', 'token',
'assets_amount', 'date_created', 'created_by', 'assets_amount', 'date_created', 'created_by',
'home', 'system_groups' 'home', 'system_groups', 'ad_domain'
] ]
extra_kwargs = { extra_kwargs = {
'password': {"write_only": True}, 'password': {"write_only": True},
'public_key': {"write_only": True}, 'public_key': {"write_only": True},
'private_key': {"write_only": True}, 'private_key': {"write_only": True},
'token': {"write_only": True}, 'token': {"write_only": True},
'nodes_amount': {'label': _('Node')}, 'nodes_amount': {'label': _('Nodes amount')},
'assets_amount': {'label': _('Asset')}, 'assets_amount': {'label': _('Assets amount')},
'login_mode_display': {'label': _('Login mode display')}, 'login_mode_display': {'label': _('Login mode display')},
'created_by': {'read_only': True}, 'created_by': {'read_only': True},
'ad_domain': {'label': _('Ad domain')},
} }
def validate_auto_push(self, value): def validate_auto_push(self, value):
@ -154,14 +154,18 @@ class SystemUserListSerializer(SystemUserSerializer):
'priority', "username_same_with_user", 'priority', "username_same_with_user",
'auto_push', 'sudo', 'shell', 'comment', 'auto_push', 'sudo', 'shell', 'comment',
"assets_amount", 'home', 'system_groups', "assets_amount", 'home', 'system_groups',
'auto_generate_key', 'auto_generate_key', 'ad_domain',
'sftp_root', 'sftp_root',
] ]
extra_kwargs = { extra_kwargs = {
'password': {"write_only": True}, 'password': {"write_only": True},
'public_key': {"write_only": True}, 'public_key': {"write_only": True},
'private_key': {"write_only": True}, 'private_key': {"write_only": True},
'nodes_amount': {'label': _('Nodes amount')},
'assets_amount': {'label': _('Assets amount')},
'login_mode_display': {'label': _('Login mode display')},
'created_by': {'read_only': True},
'ad_domain': {'label': _('Ad domain')},
} }
@classmethod @classmethod
@ -179,7 +183,8 @@ class SystemUserWithAuthInfoSerializer(SystemUserSerializer):
'login_mode', 'login_mode_display', 'login_mode', 'login_mode_display',
'priority', 'username_same_with_user', 'priority', 'username_same_with_user',
'auto_push', 'sudo', 'shell', 'comment', 'auto_push', 'sudo', 'shell', 'comment',
'auto_generate_key', 'sftp_root', 'token' 'auto_generate_key', 'sftp_root', 'token',
'ad_domain',
] ]
extra_kwargs = { extra_kwargs = {
'nodes_amount': {'label': _('Node')}, 'nodes_amount': {'label': _('Node')},

View File

@ -58,7 +58,8 @@ def on_asset_created_or_update(sender, instance=None, created=False, **kwargs):
@receiver(post_save, sender=SystemUser, dispatch_uid="jms") @receiver(post_save, sender=SystemUser, dispatch_uid="jms")
def on_system_user_update(sender, instance=None, created=True, **kwargs): @on_transaction_commit
def on_system_user_update(instance: SystemUser, created, **kwargs):
""" """
当系统用户更新时可能更新了秘钥用户名等这时要自动推送系统用户到资产上, 当系统用户更新时可能更新了秘钥用户名等这时要自动推送系统用户到资产上,
其实应该当 用户名密码秘钥 sudo等更新时再推送这里偷个懒, 其实应该当 用户名密码秘钥 sudo等更新时再推送这里偷个懒,
@ -68,26 +69,25 @@ def on_system_user_update(sender, instance=None, created=True, **kwargs):
if instance and not created: if instance and not created:
logger.info("System user update signal recv: {}".format(instance)) logger.info("System user update signal recv: {}".format(instance))
assets = instance.assets.all().valid() assets = instance.assets.all().valid()
push_system_user_to_assets.delay(instance, assets) push_system_user_to_assets.delay(instance.id, [_asset.id for _asset in assets])
@receiver(m2m_changed, sender=SystemUser.assets.through) @receiver(m2m_changed, sender=SystemUser.assets.through)
def on_system_user_assets_change(sender, instance=None, action='', model=None, pk_set=None, **kwargs): def on_system_user_assets_change(instance, action, model, pk_set, **kwargs):
""" """
当系统用户和资产关系发生变化时应该重新推送系统用户到新添加的资产中 当系统用户和资产关系发生变化时应该重新推送系统用户到新添加的资产中
""" """
if action != POST_ADD: if action != POST_ADD:
return return
logger.debug("System user assets change signal recv: {}".format(instance)) logger.debug("System user assets change signal recv: {}".format(instance))
queryset = model.objects.filter(pk__in=pk_set)
if model == Asset: if model == Asset:
system_users = [instance] system_users_id = [instance.id]
assets = queryset assets_id = pk_set
else: else:
system_users = queryset system_users_id = pk_set
assets = [instance] assets_id = [instance.id]
for system_user in system_users: for system_user_id in system_users_id:
push_system_user_to_assets.delay(system_user, assets) push_system_user_to_assets.delay(system_user_id, assets_id)
@receiver(m2m_changed, sender=SystemUser.users.through) @receiver(m2m_changed, sender=SystemUser.users.through)
@ -140,7 +140,7 @@ def on_system_user_groups_change(instance, action, pk_set, reverse, **kwargs):
logger.info("System user groups update signal recv: {}".format(instance)) logger.info("System user groups update signal recv: {}".format(instance))
users = User.objects.filter(groups__id__in=pk_set).distinct() users = User.objects.filter(groups__id__in=pk_set).distinct()
instance.users.add(users) instance.users.add(*users)
@receiver(m2m_changed, sender=Asset.nodes.through) @receiver(m2m_changed, sender=Asset.nodes.through)

View File

@ -2,7 +2,7 @@
from itertools import groupby from itertools import groupby
from celery import shared_task from celery import shared_task
from common.db.utils import get_object_if_need, get_objects_if_need from common.db.utils import get_object_if_need, get_objects_if_need, get_objects
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.db.models import Empty from django.db.models import Empty
@ -36,6 +36,7 @@ def get_push_unixlike_system_user_tasks(system_user, username=None):
username = system_user.username username = system_user.username
password = system_user.password password = system_user.password
public_key = system_user.public_key public_key = system_user.public_key
comment = system_user.name
groups = _split_by_comma(system_user.system_groups) groups = _split_by_comma(system_user.system_groups)
@ -47,7 +48,8 @@ def get_push_unixlike_system_user_tasks(system_user, username=None):
'shell': system_user.shell or Empty, 'shell': system_user.shell or Empty,
'state': 'present', 'state': 'present',
'home': system_user.home or Empty, 'home': system_user.home or Empty,
'groups': groups or Empty 'groups': groups or Empty,
'comment': comment
} }
tasks = [ tasks = [
@ -64,24 +66,27 @@ def get_push_unixlike_system_user_tasks(system_user, username=None):
'module': 'group', 'module': 'group',
'args': 'name={} state=present'.format(username), 'args': 'name={} state=present'.format(username),
} }
},
{
'name': 'Check home dir exists',
'action': {
'module': 'stat',
'args': 'path=/home/{}'.format(username)
},
'register': 'home_existed'
},
{
'name': "Set home dir permission",
'action': {
'module': 'file',
'args': "path=/home/{0} owner={0} group={0} mode=700".format(username)
},
'when': 'home_existed.stat.exists == true'
} }
] ]
if not system_user.home:
tasks.extend([
{
'name': 'Check home dir exists',
'action': {
'module': 'stat',
'args': 'path=/home/{}'.format(username)
},
'register': 'home_existed'
},
{
'name': "Set home dir permission",
'action': {
'module': 'file',
'args': "path=/home/{0} owner={0} group={0} mode=700".format(username)
},
'when': 'home_existed.stat.exists == true'
}
])
if password: if password:
tasks.append({ tasks.append({
'name': 'Set {} password'.format(username), 'name': 'Set {} password'.format(username),
@ -240,10 +245,10 @@ def push_system_user_a_asset_manual(system_user, asset, username=None):
@shared_task(queue="ansible") @shared_task(queue="ansible")
def push_system_user_to_assets(system_user, assets, username=None): def push_system_user_to_assets(system_user_id, assets_id, username=None):
system_user = SystemUser.objects.get(id=system_user_id)
assets = get_objects(Asset, assets_id)
task_name = _("Push system users to assets: {}").format(system_user.name) task_name = _("Push system users to assets: {}").format(system_user.name)
system_user = get_object_if_need(SystemUser, system_user)
assets = get_objects_if_need(Asset, assets)
return push_system_user_util(system_user, assets, task_name, username=username) return push_system_user_util(system_user, assets, task_name, username=username)
# @shared_task # @shared_task

View File

@ -45,6 +45,7 @@ urlpatterns = [
path('admin-users/<uuid:pk>/assets/', api.AdminUserAssetsListView.as_view(), name='admin-user-assets'), path('admin-users/<uuid:pk>/assets/', api.AdminUserAssetsListView.as_view(), name='admin-user-assets'),
path('system-users/<uuid:pk>/auth-info/', api.SystemUserAuthInfoApi.as_view(), name='system-user-auth-info'), path('system-users/<uuid:pk>/auth-info/', api.SystemUserAuthInfoApi.as_view(), name='system-user-auth-info'),
path('system-users/<uuid:pk>/assets/', api.SystemUserAssetsListView.as_view(), name='system-user-assets'),
path('system-users/<uuid:pk>/assets/<uuid:aid>/auth-info/', api.SystemUserAssetAuthInfoApi.as_view(), name='system-user-asset-auth-info'), path('system-users/<uuid:pk>/assets/<uuid:aid>/auth-info/', api.SystemUserAssetAuthInfoApi.as_view(), name='system-user-asset-auth-info'),
path('system-users/<uuid:pk>/tasks/', api.SystemUserTaskApi.as_view(), name='system-user-task-create'), path('system-users/<uuid:pk>/tasks/', api.SystemUserTaskApi.as_view(), name='system-user-task-create'),
path('system-users/<uuid:pk>/cmd-filter-rules/', api.SystemUserCommandFilterRuleListApi.as_view(), name='system-user-cmd-filter-rule-list'), path('system-users/<uuid:pk>/cmd-filter-rules/', api.SystemUserCommandFilterRuleListApi.as_view(), name='system-user-cmd-filter-rule-list'),

View File

@ -33,7 +33,7 @@ def is_asset_exists_in_node(asset_pk, node_key):
def is_query_node_all_assets(request): def is_query_node_all_assets(request):
request = request request = request
query_all_arg = request.query_params.get('all') query_all_arg = request.query_params.get('all', 'true')
show_current_asset_arg = request.query_params.get('show_current_asset') show_current_asset_arg = request.query_params.get('show_current_asset')
if show_current_asset_arg is not None: if show_current_asset_arg is not None:
return not is_true(show_current_asset_arg) return not is_true(show_current_asset_arg)

View File

@ -12,7 +12,7 @@ from . import models
class FTPLogSerializer(serializers.ModelSerializer): class FTPLogSerializer(serializers.ModelSerializer):
operate_display = serializers.ReadOnlyField(source='get_operate_display') operate_display = serializers.ReadOnlyField(source='get_operate_display', label=_('Operate for display'))
class Meta: class Meta:
model = models.FTPLog model = models.FTPLog
@ -23,9 +23,9 @@ class FTPLogSerializer(serializers.ModelSerializer):
class UserLoginLogSerializer(serializers.ModelSerializer): class UserLoginLogSerializer(serializers.ModelSerializer):
type_display = serializers.ReadOnlyField(source='get_type_display') type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type for display'))
status_display = serializers.ReadOnlyField(source='get_status_display') status_display = serializers.ReadOnlyField(source='get_status_display', label=_('Status for display'))
mfa_display = serializers.ReadOnlyField(source='get_mfa_display') mfa_display = serializers.ReadOnlyField(source='get_mfa_display', label=_('MFA for display'))
class Meta: class Meta:
model = models.UserLoginLog model = models.UserLoginLog
@ -33,6 +33,9 @@ class UserLoginLogSerializer(serializers.ModelSerializer):
'id', 'username', 'type', 'type_display', 'ip', 'city', 'user_agent', 'id', 'username', 'type', 'type_display', 'ip', 'city', 'user_agent',
'mfa', 'reason', 'status', 'status_display', 'datetime', 'mfa_display' 'mfa', 'reason', 'status', 'status_display', 'datetime', 'mfa_display'
) )
extra_kwargs = {
"user_agent": {'label': _('User agent')}
}
class OperateLogSerializer(serializers.ModelSerializer): class OperateLogSerializer(serializers.ModelSerializer):
@ -75,6 +78,8 @@ class CommandExecutionSerializer(serializers.ModelSerializer):
'hosts': {'label': _('Hosts')}, # 外键,会生成 sql。不在 model 上修改 'hosts': {'label': _('Hosts')}, # 外键,会生成 sql。不在 model 上修改
'run_as': {'label': _('Run as')}, 'run_as': {'label': _('Run as')},
'user': {'label': _('User')}, 'user': {'label': _('User')},
'run_as_display': {'label': _('Run as for display')},
'user_display': {'label': _('User for display')},
} }
@classmethod @classmethod

View File

@ -2,32 +2,44 @@
# #
import datetime import datetime
from django.utils import timezone from django.utils import timezone
from django.conf import settings
from celery import shared_task from celery import shared_task
from ops.celery.decorator import register_as_period_task from ops.celery.decorator import (
register_as_period_task, after_app_shutdown_clean_periodic
)
from .models import UserLoginLog, OperateLog from .models import UserLoginLog, OperateLog
from common.utils import get_log_keep_day
@register_as_period_task(interval=3600*24)
@shared_task @shared_task
@after_app_shutdown_clean_periodic
def clean_login_log_period(): def clean_login_log_period():
now = timezone.now() now = timezone.now()
try: days = get_log_keep_day('LOGIN_LOG_KEEP_DAYS')
days = int(settings.LOGIN_LOG_KEEP_DAYS)
except ValueError:
days = 9999
expired_day = now - datetime.timedelta(days=days) expired_day = now - datetime.timedelta(days=days)
UserLoginLog.objects.filter(datetime__lt=expired_day).delete() UserLoginLog.objects.filter(datetime__lt=expired_day).delete()
@register_as_period_task(interval=3600*24)
@shared_task @shared_task
@after_app_shutdown_clean_periodic
def clean_operation_log_period(): def clean_operation_log_period():
now = timezone.now() now = timezone.now()
try: days = get_log_keep_day('OPERATE_LOG_KEEP_DAYS')
days = int(settings.LOGIN_LOG_KEEP_DAYS)
except ValueError:
days = 9999
expired_day = now - datetime.timedelta(days=days) expired_day = now - datetime.timedelta(days=days)
OperateLog.objects.filter(datetime__lt=expired_day).delete() OperateLog.objects.filter(datetime__lt=expired_day).delete()
@shared_task
def clean_ftp_log_period():
now = timezone.now()
days = get_log_keep_day('FTP_LOG_KEEP_DAYS')
expired_day = now - datetime.timedelta(days=days)
OperateLog.objects.filter(datetime__lt=expired_day).delete()
@register_as_period_task(interval=3600*24)
@shared_task
def clean_audits_log_period():
clean_audits_log_period()
clean_operation_log_period()
clean_ftp_log_period()

View File

@ -60,6 +60,7 @@ class SSOViewSet(AuthMixin, JmsGenericViewSet):
此接口违反了 `Restful` 的规范 此接口违反了 `Restful` 的规范
`GET` 应该是安全的方法但此接口是不安全的 `GET` 应该是安全的方法但此接口是不安全的
""" """
request.META['HTTP_X_JMS_LOGIN_TYPE'] = 'W'
authkey = request.query_params.get(AUTH_KEY) authkey = request.query_params.get(AUTH_KEY)
next_url = request.query_params.get(NEXT_URL) next_url = request.query_params.get(NEXT_URL)
if not next_url or not next_url.startswith('/'): if not next_url or not next_url.startswith('/'):

View File

@ -53,7 +53,7 @@ class AuthMixin:
ip = ip or get_request_ip(self.request) ip = ip or get_request_ip(self.request)
return ip return ip
def check_is_block(self): def check_is_block(self, raise_exception=True):
if hasattr(self.request, 'data'): if hasattr(self.request, 'data'):
username = self.request.data.get("username") username = self.request.data.get("username")
else: else:
@ -61,7 +61,11 @@ class AuthMixin:
ip = self.get_request_ip() ip = self.get_request_ip()
if is_block_login(username, ip): if is_block_login(username, ip):
logger.warn('Ip was blocked' + ': ' + username + ':' + ip) logger.warn('Ip was blocked' + ': ' + username + ':' + ip)
raise errors.BlockLoginError(username=username, ip=ip) exception = errors.BlockLoginError(username=username, ip=ip)
if raise_exception:
raise errors.BlockLoginError(username=username, ip=ip)
else:
return exception
def decrypt_passwd(self, raw_passwd): def decrypt_passwd(self, raw_passwd):
# 获取解密密钥,对密码进行解密 # 获取解密密钥,对密码进行解密

View File

@ -20,7 +20,7 @@
<div class="form-group"> <div class="form-group">
<input type="text" class="form-control" name="otp_code" placeholder="" required="" autofocus="autofocus"> <input type="text" class="form-control" name="otp_code" placeholder="" required="" autofocus="autofocus">
<span class="help-block"> <span class="help-block">
{% trans 'Open Google Authenticator and enter the 6-bit dynamic code' %} {% trans 'Open MFA Authenticator and enter the 6-bit dynamic code' %}
</span> </span>
</div> </div>
<button type="submit" class="btn btn-primary block full-width m-b">{% trans 'Next' %}</button> <button type="submit" class="btn btn-primary block full-width m-b">{% trans 'Next' %}</button>

View File

@ -87,6 +87,7 @@ class UserLoginView(mixins.AuthMixin, FormView):
try: try:
self.check_user_auth(decrypt_passwd=True) self.check_user_auth(decrypt_passwd=True)
except errors.AuthFailedError as e: except errors.AuthFailedError as e:
e = self.check_is_block(raise_exception=False) or e
form.add_error(None, e.msg) form.add_error(None, e.msg)
ip = self.get_request_ip() ip = self.get_request_ip()
cache.set(self.key_prefix_captcha.format(ip), 1, 3600) cache.set(self.key_prefix_captcha.format(ip), 1, 3600)

View File

@ -25,3 +25,16 @@ def get_objects_if_need(model, pks):
logger.error(f'DoesNotExist: <{model.__name__}: {not_found_pks}>') logger.error(f'DoesNotExist: <{model.__name__}: {not_found_pks}>')
return objs return objs
return pks return pks
def get_objects(model, pks):
if not pks:
return pks
objs = list(model.objects.filter(id__in=pks))
if len(objs) != len(pks):
pks = set(pks)
exists_pks = {o.id for o in objs}
not_found_pks = ','.join(pks - exists_pks)
logger.error(f'DoesNotExist: <{model.__name__}: {not_found_pks}>')
return objs

View File

@ -6,9 +6,10 @@ from rest_framework.views import set_rollback
from rest_framework.response import Response from rest_framework.response import Response
from common.exceptions import JMSObjectDoesNotExist from common.exceptions import JMSObjectDoesNotExist
from common.utils import get_logger from logging import getLogger
logger = get_logger(__name__) logger = getLogger('drf_exception')
unexpected_exception_logger = getLogger('unexpected_exception')
def extract_object_name(exc, index=0): def extract_object_name(exc, index=0):
@ -38,12 +39,14 @@ def common_exception_handler(exc, context):
if getattr(exc, 'wait', None): if getattr(exc, 'wait', None):
headers['Retry-After'] = '%d' % exc.wait headers['Retry-After'] = '%d' % exc.wait
if isinstance(exc.detail, (list, dict)): if isinstance(exc.detail, str) and isinstance(exc.get_codes(), str):
data = exc.detail data = {'detail': exc.detail, 'code': exc.get_codes()}
else: else:
data = {'detail': exc.detail} data = exc.detail
set_rollback() set_rollback()
return Response(data, status=exc.status_code, headers=headers) return Response(data, status=exc.status_code, headers=headers)
else:
unexpected_exception_logger.exception('')
return None return None

View File

@ -7,6 +7,7 @@ from collections import OrderedDict
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.http import Http404 from django.http import Http404
from django.utils.encoding import force_text from django.utils.encoding import force_text
from rest_framework.fields import empty
from rest_framework.metadata import SimpleMetadata from rest_framework.metadata import SimpleMetadata
from rest_framework import exceptions, serializers from rest_framework import exceptions, serializers
@ -58,6 +59,10 @@ class SimpleMetadataWithFilters(SimpleMetadata):
field_info['type'] = self.label_lookup[field] field_info['type'] = self.label_lookup[field]
field_info['required'] = getattr(field, 'required', False) field_info['required'] = getattr(field, 'required', False)
default = getattr(field, 'default', False)
if default and isinstance(default, (str, int)):
field_info['default'] = default
for attr in self.attrs: for attr in self.attrs:
value = getattr(field, attr, None) value = getattr(field, attr, None)
if value is not None and value != '': if value is not None and value != '':

View File

@ -5,7 +5,7 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import force_text from django.utils.encoding import force_text
from ..utils import signer, aes_crypto, aes_ecb_crypto from ..utils import signer, crypto
__all__ = [ __all__ = [
@ -116,27 +116,12 @@ class EncryptMixin:
def decrypt_from_signer(self, value): def decrypt_from_signer(self, value):
return signer.unsign(value) or '' return signer.unsign(value) or ''
def decrypt_from_aes(self, value):
"""
先尝试使用GCM模式解密如果解不开再尝试使用原来的ECB模式解密
"""
try:
return aes_crypto.decrypt(value)
except ValueError:
pass
try:
return aes_ecb_crypto.decrypt(value)
except (TypeError, ValueError, UnicodeDecodeError):
pass
def from_db_value(self, value, expression, connection, context): def from_db_value(self, value, expression, connection, context):
if value is None: if value is None:
return value return value
value = force_text(value) value = force_text(value)
# 优先采用 aes 解密 plain_value = crypto.decrypt(value)
plain_value = self.decrypt_from_aes(value)
# 如果没有解开使用原来的signer解密 # 如果没有解开使用原来的signer解密
if not plain_value: if not plain_value:
@ -158,7 +143,7 @@ class EncryptMixin:
value = sp.get_prep_value(value) value = sp.get_prep_value(value)
value = force_text(value) value = force_text(value)
# 替换新的加密方式 # 替换新的加密方式
return aes_crypto.encrypt(value) return crypto.encrypt(value)
class EncryptTextField(EncryptMixin, models.TextField): class EncryptTextField(EncryptMixin, models.TextField):

View File

@ -11,8 +11,6 @@ import time
import ipaddress import ipaddress
import psutil import psutil
from .timezone import dt_formater
UUID_PATTERN = re.compile(r'\w{8}(-\w{4}){3}-\w{12}') UUID_PATTERN = re.compile(r'\w{8}(-\w{4}){3}-\w{12}')
ipip_db = None ipip_db = None

View File

@ -2,8 +2,58 @@ import base64
from Crypto.Cipher import AES from Crypto.Cipher import AES
from Crypto.Util.Padding import pad from Crypto.Util.Padding import pad
from Crypto.Random import get_random_bytes from Crypto.Random import get_random_bytes
from gmssl.sm4 import CryptSM4, SM4_ENCRYPT, SM4_DECRYPT
from django.conf import settings from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
def process_key(key):
"""
返回32 bytes 的key
"""
if not isinstance(key, bytes):
key = bytes(key, encoding='utf-8')
if len(key) >= 32:
return key[:32]
return pad(key, 32)
class BaseCrypto:
def encrypt(self, text):
return base64.urlsafe_b64encode(
self._encrypt(bytes(text, encoding='utf8'))
).decode('utf8')
def _encrypt(self, data: bytes) -> bytes:
raise NotImplementedError
def decrypt(self, text):
return self._decrypt(
base64.urlsafe_b64decode(bytes(text, encoding='utf8'))
).decode('utf8')
def _decrypt(self, data: bytes) -> bytes:
raise NotImplementedError
class GMSM4EcbCrypto(BaseCrypto):
def __init__(self, key):
self.key = process_key(key)
self.sm4_encryptor = CryptSM4()
self.sm4_encryptor.set_key(self.key, SM4_ENCRYPT)
self.sm4_decryptor = CryptSM4()
self.sm4_decryptor.set_key(self.key, SM4_DECRYPT)
def _encrypt(self, data: bytes) -> bytes:
return self.sm4_encryptor.crypt_ecb(data)
def _decrypt(self, data: bytes) -> bytes:
return self.sm4_decryptor.crypt_ecb(data)
class AESCrypto: class AESCrypto:
@ -52,20 +102,7 @@ class AESCryptoGCM:
""" """
def __init__(self, key): def __init__(self, key):
self.key = self.process_key(key) self.key = process_key(key)
@staticmethod
def process_key(key):
"""
返回32 bytes 的key
"""
if not isinstance(key, bytes):
key = bytes(key, encoding='utf-8')
if len(key) >= 32:
return key[:32]
return pad(key, 32)
def encrypt(self, text): def encrypt(self, text):
""" """
@ -110,5 +147,50 @@ def get_aes_crypto(key=None, mode='GCM'):
return a return a
def get_gm_sm4_ecb_crypto(key=None):
key = key or settings.SECRET_KEY
return GMSM4EcbCrypto(key)
aes_ecb_crypto = get_aes_crypto(mode='ECB') aes_ecb_crypto = get_aes_crypto(mode='ECB')
aes_crypto = get_aes_crypto(mode='GCM') aes_crypto = get_aes_crypto(mode='GCM')
gm_sm4_ecb_crypto = get_gm_sm4_ecb_crypto()
class Crypto:
cryptoes = {
'aes_ecb': aes_ecb_crypto,
'aes_gcm': aes_crypto,
'aes': aes_crypto,
'gm_sm4_ecb': gm_sm4_ecb_crypto,
'gm': gm_sm4_ecb_crypto,
}
def __init__(self):
cryptoes = self.__class__.cryptoes.copy()
crypto = cryptoes.pop(settings.SECURITY_DATA_CRYPTO_ALGO, None)
if crypto is None:
raise ImproperlyConfigured(
f'Crypto method not supported {settings.SECURITY_DATA_CRYPTO_ALGO}'
)
self.cryptoes = [crypto, *cryptoes.values()]
@property
def encryptor(self):
return self.cryptoes[0]
def encrypt(self, text):
return self.encryptor.encrypt(text)
def decrypt(self, text):
for decryptor in self.cryptoes:
try:
origin_text = decryptor.decrypt(text)
if origin_text:
# 有时不同算法解密不报错,但是返回空字符串
return origin_text
except (TypeError, ValueError, UnicodeDecodeError):
continue
crypto = Crypto()

View File

@ -10,13 +10,15 @@ UUID_PATTERN = re.compile(r'[0-9a-zA-Z\-]{36}')
def reverse(view_name, urlconf=None, args=None, kwargs=None, def reverse(view_name, urlconf=None, args=None, kwargs=None,
current_app=None, external=False): current_app=None, external=False, api_to_ui=False):
url = dj_reverse(view_name, urlconf=urlconf, args=args, url = dj_reverse(view_name, urlconf=urlconf, args=args,
kwargs=kwargs, current_app=current_app) kwargs=kwargs, current_app=current_app)
if external: if external:
site_url = settings.SITE_URL site_url = settings.SITE_URL
url = site_url.strip('/') + url url = site_url.strip('/') + url
if api_to_ui:
url = url.replace('api/v1', 'ui/#').rstrip('/')
return url return url
@ -48,3 +50,11 @@ def union_queryset(*args, base_queryset=None):
base_queryset = args[0].model.objects base_queryset = args[0].model.objects
queryset = base_queryset.filter(id__in=queryset_id) queryset = base_queryset.filter(id__in=queryset_id)
return queryset return queryset
def get_log_keep_day(s, defaults=200):
try:
days = int(getattr(settings, s))
except ValueError:
days = defaults
return days

View File

@ -0,0 +1,5 @@
import re
def no_special_chars(s):
return bool(re.match(r'\w+$', s))

View File

@ -2,10 +2,12 @@
# #
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework.validators import ( from rest_framework.validators import (
UniqueTogetherValidator, ValidationError UniqueTogetherValidator, ValidationError
) )
from rest_framework import serializers
from common.utils.strings import no_special_chars
alphanumeric = RegexValidator(r'^[0-9a-zA-Z_@\-\.]*$', _('Special char not allowed')) alphanumeric = RegexValidator(r'^[0-9a-zA-Z_@\-\.]*$', _('Special char not allowed'))
@ -22,3 +24,11 @@ class ProjectUniqueValidator(UniqueTogetherValidator):
continue continue
errors[field] = _('This field must be unique.') errors[field] = _('This field must be unique.')
raise ValidationError(errors) raise ValidationError(errors)
class NoSpecialChars:
def __call__(self, value):
if not no_special_chars(value):
raise serializers.ValidationError(
_("Should not contains special characters")
)

View File

@ -224,7 +224,7 @@ class Config(dict):
'TERMINAL_HEARTBEAT_INTERVAL': 20, 'TERMINAL_HEARTBEAT_INTERVAL': 20,
'TERMINAL_ASSET_LIST_SORT_BY': 'hostname', 'TERMINAL_ASSET_LIST_SORT_BY': 'hostname',
'TERMINAL_ASSET_LIST_PAGE_SIZE': 'auto', 'TERMINAL_ASSET_LIST_PAGE_SIZE': 'auto',
'TERMINAL_SESSION_KEEP_DURATION': 9999, 'TERMINAL_SESSION_KEEP_DURATION': 200,
'TERMINAL_HOST_KEY': '', 'TERMINAL_HOST_KEY': '',
'TERMINAL_TELNET_REGEX': '', 'TERMINAL_TELNET_REGEX': '',
'TERMINAL_COMMAND_STORAGE': {}, 'TERMINAL_COMMAND_STORAGE': {},
@ -244,12 +244,18 @@ class Config(dict):
'SECURITY_PASSWORD_SPECIAL_CHAR': False, 'SECURITY_PASSWORD_SPECIAL_CHAR': False,
'SECURITY_LOGIN_CHALLENGE_ENABLED': False, 'SECURITY_LOGIN_CHALLENGE_ENABLED': False,
'SECURITY_LOGIN_CAPTCHA_ENABLED': True, 'SECURITY_LOGIN_CAPTCHA_ENABLED': True,
'SECURITY_DATA_CRYPTO_ALGO': 'aes',
'SECURITY_INSECURE_COMMAND': False,
'SECURITY_INSECURE_COMMAND_LEVEL': 5,
'SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER': '',
'HTTP_BIND_HOST': '0.0.0.0', 'HTTP_BIND_HOST': '0.0.0.0',
'HTTP_LISTEN_PORT': 8080, 'HTTP_LISTEN_PORT': 8080,
'WS_LISTEN_PORT': 8070, 'WS_LISTEN_PORT': 8070,
'LOGIN_LOG_KEEP_DAYS': 9999, 'LOGIN_LOG_KEEP_DAYS': 200,
'TASK_LOG_KEEP_DAYS': 10, 'TASK_LOG_KEEP_DAYS': 90,
'OPERATE_LOG_KEEP_DAYS': 200,
'FTP_LOG_KEEP_DAYS': 200,
'ASSETS_PERM_CACHE_TIME': 3600 * 24, 'ASSETS_PERM_CACHE_TIME': 3600 * 24,
'SECURITY_MFA_VERIFY_TTL': 3600, 'SECURITY_MFA_VERIFY_TTL': 3600,
'ASSETS_PERM_CACHE_ENABLE': HAS_XPACK, 'ASSETS_PERM_CACHE_ENABLE': HAS_XPACK,
@ -366,7 +372,7 @@ class Config(dict):
tp = type(default_value) tp = type(default_value)
# 对bool特殊处理 # 对bool特殊处理
if tp is bool and isinstance(v, str): if tp is bool and isinstance(v, str):
if v in ("true", "True", "1"): if v.lower() in ("true", "1"):
return True return True
else: else:
return False return False

View File

@ -242,6 +242,9 @@ CACHES = {
'host': CONFIG.REDIS_HOST, 'host': CONFIG.REDIS_HOST,
'port': CONFIG.REDIS_PORT, 'port': CONFIG.REDIS_PORT,
'db': CONFIG.REDIS_DB_CACHE, 'db': CONFIG.REDIS_DB_CACHE,
},
'OPTIONS': {
"REDIS_CLIENT_KWARGS": {"health_check_interval": 30}
} }
} }
} }

View File

@ -54,6 +54,10 @@ SECURITY_VIEW_AUTH_NEED_MFA = CONFIG.SECURITY_VIEW_AUTH_NEED_MFA
SECURITY_SERVICE_ACCOUNT_REGISTRATION = DYNAMIC.SECURITY_SERVICE_ACCOUNT_REGISTRATION SECURITY_SERVICE_ACCOUNT_REGISTRATION = DYNAMIC.SECURITY_SERVICE_ACCOUNT_REGISTRATION
SECURITY_LOGIN_CAPTCHA_ENABLED = CONFIG.SECURITY_LOGIN_CAPTCHA_ENABLED SECURITY_LOGIN_CAPTCHA_ENABLED = CONFIG.SECURITY_LOGIN_CAPTCHA_ENABLED
SECURITY_LOGIN_CHALLENGE_ENABLED = CONFIG.SECURITY_LOGIN_CHALLENGE_ENABLED SECURITY_LOGIN_CHALLENGE_ENABLED = CONFIG.SECURITY_LOGIN_CHALLENGE_ENABLED
SECURITY_DATA_CRYPTO_ALGO = CONFIG.SECURITY_DATA_CRYPTO_ALGO
SECURITY_INSECURE_COMMAND = DYNAMIC.SECURITY_INSECURE_COMMAND
SECURITY_INSECURE_COMMAND_LEVEL = CONFIG.SECURITY_INSECURE_COMMAND_LEVEL
SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER = DYNAMIC.SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER
# Terminal other setting # Terminal other setting
TERMINAL_PASSWORD_AUTH = DYNAMIC.TERMINAL_PASSWORD_AUTH TERMINAL_PASSWORD_AUTH = DYNAMIC.TERMINAL_PASSWORD_AUTH

View File

@ -5,6 +5,8 @@ from ..const import PROJECT_DIR, CONFIG
LOG_DIR = os.path.join(PROJECT_DIR, 'logs') LOG_DIR = os.path.join(PROJECT_DIR, 'logs')
JUMPSERVER_LOG_FILE = os.path.join(LOG_DIR, 'jumpserver.log') JUMPSERVER_LOG_FILE = os.path.join(LOG_DIR, 'jumpserver.log')
DRF_EXCEPTION_LOG_FILE = os.path.join(LOG_DIR, 'drf_exception.log')
UNEXPECTED_EXCEPTION_LOG_FILE = os.path.join(LOG_DIR, 'unexpected_exception.log')
ANSIBLE_LOG_FILE = os.path.join(LOG_DIR, 'ansible.log') ANSIBLE_LOG_FILE = os.path.join(LOG_DIR, 'ansible.log')
GUNICORN_LOG_FILE = os.path.join(LOG_DIR, 'gunicorn.log') GUNICORN_LOG_FILE = os.path.join(LOG_DIR, 'gunicorn.log')
LOG_LEVEL = CONFIG.LOG_LEVEL LOG_LEVEL = CONFIG.LOG_LEVEL
@ -20,6 +22,10 @@ LOGGING = {
'datefmt': '%Y-%m-%d %H:%M:%S', 'datefmt': '%Y-%m-%d %H:%M:%S',
'format': '%(asctime)s [%(module)s %(levelname)s] %(message)s', 'format': '%(asctime)s [%(module)s %(levelname)s] %(message)s',
}, },
'exception': {
'datefmt': '%Y-%m-%d %H:%M:%S',
'format': '\n%(asctime)s [%(levelname)s] %(message)s',
},
'simple': { 'simple': {
'format': '%(levelname)s %(message)s' 'format': '%(levelname)s %(message)s'
}, },
@ -58,6 +64,24 @@ LOGGING = {
'backupCount': 7, 'backupCount': 7,
'filename': ANSIBLE_LOG_FILE, 'filename': ANSIBLE_LOG_FILE,
}, },
'drf_exception': {
'encoding': 'utf8',
'level': 'DEBUG',
'class': 'logging.handlers.RotatingFileHandler',
'formatter': 'exception',
'maxBytes': 1024 * 1024 * 100,
'backupCount': 7,
'filename': DRF_EXCEPTION_LOG_FILE,
},
'unexpected_exception': {
'encoding': 'utf8',
'level': 'DEBUG',
'class': 'logging.handlers.RotatingFileHandler',
'formatter': 'exception',
'maxBytes': 1024 * 1024 * 100,
'backupCount': 7,
'filename': UNEXPECTED_EXCEPTION_LOG_FILE,
},
'syslog': { 'syslog': {
'level': 'INFO', 'level': 'INFO',
'class': 'logging.NullHandler', 'class': 'logging.NullHandler',
@ -84,6 +108,14 @@ LOGGING = {
'handlers': ['console', 'file'], 'handlers': ['console', 'file'],
'level': LOG_LEVEL, 'level': LOG_LEVEL,
}, },
'drf_exception': {
'handlers': ['console', 'drf_exception'],
'level': LOG_LEVEL,
},
'unexpected_exception': {
'handlers': ['unexpected_exception'],
'level': LOG_LEVEL,
},
'ops.ansible_api': { 'ops.ansible_api': {
'handlers': ['console', 'ansible_logs'], 'handlers': ['console', 'ansible_logs'],
'level': LOG_LEVEL, 'level': LOG_LEVEL,

View File

@ -4,6 +4,8 @@ from __future__ import unicode_literals
from django.urls import path, include, re_path from django.urls import path, include, re_path
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static
from django.conf.urls.i18n import i18n_patterns
from django.views.i18n import JavaScriptCatalog
from . import views, api from . import views, api
@ -66,7 +68,11 @@ urlpatterns = [
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \ urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \
+ static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
# urlpatterns += js_i18n_patterns
js_i18n_patterns = i18n_patterns(
path('jsi18n/', JavaScriptCatalog.as_view(), name='javascript-catalog'),
)
urlpatterns += js_i18n_patterns
handler404 = 'jumpserver.views.handler404' handler404 = 'jumpserver.views.handler404'
handler500 = 'jumpserver.views.handler500' handler500 = 'jumpserver.views.handler500'

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,7 @@ from django.shortcuts import get_object_or_404
from rest_framework import viewsets, generics from rest_framework import viewsets, generics
from rest_framework.views import Response from rest_framework.views import Response
from common.drf.api import JMSBulkModelViewSet
from common.permissions import IsOrgAdmin from common.permissions import IsOrgAdmin
from common.serializers import CeleryTaskSerializer from common.serializers import CeleryTaskSerializer
from ..models import Task, AdHoc, AdHocExecution from ..models import Task, AdHoc, AdHocExecution
@ -22,7 +23,7 @@ __all__ = [
] ]
class TaskViewSet(viewsets.ModelViewSet): class TaskViewSet(JMSBulkModelViewSet):
queryset = Task.objects.all() queryset = Task.objects.all()
filter_fields = ("name",) filter_fields = ("name",)
search_fields = filter_fields search_fields = filter_fields

View File

@ -3,6 +3,7 @@ from __future__ import unicode_literals
from rest_framework import serializers from rest_framework import serializers
from django.shortcuts import reverse from django.shortcuts import reverse
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from ..models import Task, AdHoc, AdHocExecution, CommandExecution from ..models import Task, AdHoc, AdHocExecution, CommandExecution
@ -45,7 +46,7 @@ class AdHocExecutionExcludeResultSerializer(AdHocExecutionSerializer):
] ]
class TaskSerializer(serializers.ModelSerializer): class TaskSerializer(BulkOrgResourceModelSerializer):
summary = serializers.ReadOnlyField(source='history_summary') summary = serializers.ReadOnlyField(source='history_summary')
latest_execution = AdHocExecutionExcludeResultSerializer(read_only=True) latest_execution = AdHocExecutionExcludeResultSerializer(read_only=True)

View File

@ -9,7 +9,7 @@ from celery.exceptions import SoftTimeLimitExceeded
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from common.utils import get_logger, get_object_or_none, get_disk_usage from common.utils import get_logger, get_object_or_none, get_disk_usage, get_log_keep_day
from orgs.utils import tmp_to_root_org, tmp_to_org from orgs.utils import tmp_to_root_org, tmp_to_org
from .celery.decorator import ( from .celery.decorator import (
register_as_period_task, after_app_shutdown_clean_periodic, register_as_period_task, after_app_shutdown_clean_periodic,
@ -83,10 +83,10 @@ def clean_tasks_adhoc_period():
@after_app_shutdown_clean_periodic @after_app_shutdown_clean_periodic
@register_as_period_task(interval=3600*24, description=_("Clean celery log period")) @register_as_period_task(interval=3600*24, description=_("Clean celery log period"))
def clean_celery_tasks_period(): def clean_celery_tasks_period():
expire_days = settings.TASK_LOG_KEEP_DAYS
logger.debug("Start clean celery task history") logger.debug("Start clean celery task history")
one_month_ago = timezone.now() - timezone.timedelta(days=expire_days) expire_days = get_log_keep_day('TASK_LOG_KEEP_DAYS')
tasks = CeleryTask.objects.filter(date_start__lt=one_month_ago) days_ago = timezone.now() - timezone.timedelta(days=expire_days)
tasks = CeleryTask.objects.filter(date_start__lt=days_ago)
tasks.delete() tasks.delete()
tasks = CeleryTask.objects.filter(date_start__isnull=True) tasks = CeleryTask.objects.filter(date_start__isnull=True)
tasks.delete() tasks.delete()

View File

@ -3,13 +3,16 @@ from __future__ import unicode_literals
from django.urls import path from django.urls import path
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from rest_framework_bulk.routes import BulkRouter
from .. import api from .. import api
app_name = "ops" app_name = "ops"
router = DefaultRouter() router = DefaultRouter()
router.register(r'tasks', api.TaskViewSet, 'task') bulk_router = BulkRouter()
bulk_router.register(r'tasks', api.TaskViewSet, 'task')
router.register(r'adhoc', api.AdHocViewSet, 'adhoc') router.register(r'adhoc', api.AdHocViewSet, 'adhoc')
router.register(r'adhoc-executions', api.AdHocRunHistoryViewSet, 'execution') router.register(r'adhoc-executions', api.AdHocRunHistoryViewSet, 'execution')
router.register(r'command-executions', api.CommandExecutionViewSet, 'command-execution') router.register(r'command-executions', api.CommandExecutionViewSet, 'command-execution')
@ -22,3 +25,4 @@ urlpatterns = [
] ]
urlpatterns += router.urls urlpatterns += router.urls
urlpatterns += bulk_router.urls

View File

@ -75,4 +75,3 @@ def send_server_performance_mail(path, usage, usages):
send_mail_async(subject, message, recipient_list, html_message=message) send_mail_async(subject, message, recipient_list, html_message=message)

View File

@ -7,11 +7,12 @@ from rest_framework.views import Response
from rest_framework_bulk import BulkModelViewSet from rest_framework_bulk import BulkModelViewSet
from common.permissions import IsSuperUserOrAppUser from common.permissions import IsSuperUserOrAppUser
from common.drf.api import JMSBulkRelationModelViewSet, JMSModelViewSet from common.drf.api import JMSBulkRelationModelViewSet
from .models import Organization, ROLE from .models import Organization, ROLE
from .serializers import ( from .serializers import (
OrgSerializer, OrgReadSerializer, OrgSerializer, OrgReadSerializer,
OrgRetrieveSerializer, OrgMemberSerializer OrgRetrieveSerializer, OrgMemberSerializer,
OrgMemberAdminSerializer, OrgMemberUserSerializer
) )
from users.models import User, UserGroup from users.models import User, UserGroup
from assets.models import Asset, Domain, AdminUser, SystemUser, Label from assets.models import Asset, Domain, AdminUser, SystemUser, Label
@ -72,6 +73,55 @@ class OrgMemberRelationBulkViewSet(JMSBulkRelationModelViewSet):
m2m_field = Organization.members.field m2m_field = Organization.members.field
serializer_class = OrgMemberSerializer serializer_class = OrgMemberSerializer
filterset_class = OrgMemberRelationFilterSet filterset_class = OrgMemberRelationFilterSet
search_fields = ('user__name', 'user__username', 'org__name')
def perform_bulk_create(self, serializer):
data = serializer.validated_data
relations = [OrganizationMember(**i) for i in data]
OrganizationMember.objects.bulk_create(relations, ignore_conflicts=True)
def perform_bulk_destroy(self, queryset):
objs = list(queryset.all().prefetch_related('user', 'org'))
queryset.delete()
self.send_m2m_changed_signal(objs, action='post_remove')
class OrgMemberAdminRelationBulkViewSet(JMSBulkRelationModelViewSet):
permission_classes = (IsSuperUserOrAppUser,)
m2m_field = Organization.members.field
serializer_class = OrgMemberAdminSerializer
filterset_class = OrgMemberRelationFilterSet
search_fields = ('user__name', 'user__username', 'org__name')
def get_queryset(self):
queryset = super().get_queryset()
org_id = self.kwargs.get('org_id')
queryset = queryset.filter(org_id=org_id, role=ROLE.ADMIN)
return queryset
def perform_bulk_create(self, serializer):
data = serializer.validated_data
relations = [OrganizationMember(**i) for i in data]
OrganizationMember.objects.bulk_create(relations, ignore_conflicts=True)
def perform_bulk_destroy(self, queryset):
objs = list(queryset.all().prefetch_related('user', 'org'))
queryset.delete()
self.send_m2m_changed_signal(objs, action='post_remove')
class OrgMemberUserRelationBulkViewSet(JMSBulkRelationModelViewSet):
permission_classes = (IsSuperUserOrAppUser,)
m2m_field = Organization.members.field
serializer_class = OrgMemberUserSerializer
filterset_class = OrgMemberRelationFilterSet
search_fields = ('user__name', 'user__username', 'org__name')
def get_queryset(self):
queryset = super().get_queryset()
org_id = self.kwargs.get('org_id')
queryset = queryset.filter(org_id=org_id, role=ROLE.USER)
return queryset
def perform_bulk_create(self, serializer): def perform_bulk_create(self, serializer):
data = serializer.validated_data data = serializer.validated_data

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.13 on 2020-10-23 08:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('orgs', '0008_auto_20200819_2041'),
]
operations = [
migrations.AlterField(
model_name='organization',
name='comment',
field=models.TextField(blank=True, default='', verbose_name='Comment'),
),
]

View File

@ -23,7 +23,7 @@ class Organization(models.Model):
name = models.CharField(max_length=128, unique=True, verbose_name=_("Name")) name = models.CharField(max_length=128, unique=True, verbose_name=_("Name"))
created_by = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by')) created_by = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by'))
date_created = models.DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created')) date_created = models.DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created'))
comment = models.TextField(max_length=128, default='', blank=True, verbose_name=_('Comment')) comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
members = models.ManyToManyField('users.User', related_name='orgs', through='orgs.OrganizationMember', members = models.ManyToManyField('users.User', related_name='orgs', through='orgs.OrganizationMember',
through_fields=('org', 'user')) through_fields=('org', 'user'))

View File

@ -1,12 +1,13 @@
from django.db.models import F from django.db.models import F
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer
from rest_framework import serializers from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from users.models.user import User from users.models.user import User
from common.serializers import AdaptedBulkListSerializer from common.serializers import AdaptedBulkListSerializer
from common.drf.serializers import BulkModelSerializer from common.drf.serializers import BulkModelSerializer
from common.db.models import concated_display as display from common.db.models import concated_display as display
from .models import Organization, OrganizationMember from .models import Organization, OrganizationMember, ROLE
class OrgSerializer(ModelSerializer): class OrgSerializer(ModelSerializer):
@ -26,12 +27,12 @@ class OrgSerializer(ModelSerializer):
read_only_fields = ['created_by', 'date_created'] read_only_fields = ['created_by', 'date_created']
def create(self, validated_data): def create(self, validated_data):
members = self._pop_memebers(validated_data) members = self._pop_members(validated_data)
instance = Organization.objects.create(**validated_data) instance = Organization.objects.create(**validated_data)
OrganizationMember.objects.add_users_by_role(instance, *members) OrganizationMember.objects.add_users_by_role(instance, *members)
return instance return instance
def _pop_memebers(self, validated_data): def _pop_members(self, validated_data):
return ( return (
validated_data.pop('users', None), validated_data.pop('users', None),
validated_data.pop('admins', None), validated_data.pop('admins', None),
@ -39,7 +40,7 @@ class OrgSerializer(ModelSerializer):
) )
def update(self, instance, validated_data): def update(self, instance, validated_data):
members = self._pop_memebers(validated_data) members = self._pop_members(validated_data)
for attr, value in validated_data.items(): for attr, value in validated_data.items():
setattr(instance, attr, value) setattr(instance, attr, value)
instance.save() instance.save()
@ -73,6 +74,28 @@ class OrgMemberSerializer(BulkModelSerializer):
).distinct() ).distinct()
class OrgMemberAdminSerializer(BulkModelSerializer):
role = serializers.HiddenField(default=ROLE.ADMIN)
organization = serializers.PrimaryKeyRelatedField(
label=_('Organization'), queryset=Organization.objects.all(), required=True, source='org'
)
class Meta:
model = OrganizationMember
fields = ('id', 'organization', 'user', 'role')
class OrgMemberUserSerializer(BulkModelSerializer):
role = serializers.HiddenField(default=ROLE.USER)
organization = serializers.PrimaryKeyRelatedField(
label=_('Organization'), queryset=Organization.objects.all(), required=True, source='org'
)
class Meta:
model = OrganizationMember
fields = ('id', 'organization', 'user', 'role')
class OrgRetrieveSerializer(OrgReadSerializer): class OrgRetrieveSerializer(OrgReadSerializer):
admins = serializers.PrimaryKeyRelatedField(many=True, read_only=True) admins = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
auditors = serializers.PrimaryKeyRelatedField(many=True, read_only=True) auditors = serializers.PrimaryKeyRelatedField(many=True, read_only=True)

View File

@ -14,7 +14,12 @@ router = DefaultRouter()
bulk_router = BulkRouter() bulk_router = BulkRouter()
router.register(r'orgs', api.OrgViewSet, 'org') router.register(r'orgs', api.OrgViewSet, 'org')
bulk_router.register(r'org-memeber-relation', api.OrgMemberRelationBulkViewSet, 'org-memeber-relation') bulk_router.register(r'org-member-relation', api.OrgMemberRelationBulkViewSet, 'org-member-relation')
router.register(r'orgs/(?P<org_id>[0-9a-zA-Z\-]{36})/membership/admins',
api.OrgMemberAdminRelationBulkViewSet, 'membership-admins')
router.register(r'orgs/(?P<org_id>[0-9a-zA-Z\-]{36})/membership/users',
api.OrgMemberUserRelationBulkViewSet, 'membership-users'),
old_version_urlpatterns = [ old_version_urlpatterns = [
re_path('(?P<resource>org)/.*', capi.redirect_plural_name_api) re_path('(?P<resource>org)/.*', capi.redirect_plural_name_api)

View File

@ -1,10 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from .asset_permission import * from .asset import *
from .user_permission import * from .application import *
from .asset_permission_relation import *
from .user_group_permission import * # TODO: 删除
from .remote_app_permission import * from .remote_app_permission import *
from .remote_app_permission_relation import * from .remote_app_permission_relation import *
from .user_remote_app_permission import * from .user_remote_app_permission import *

View File

@ -0,0 +1,4 @@
from .user_permission import *
from .application_permission import *
from .application_permission_relation import *
from .user_group_permission import *

View File

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
#
from common.permissions import IsOrgAdmin
from orgs.mixins.api import OrgBulkModelViewSet
from perms.models import ApplicationPermission
from perms import serializers
class ApplicationPermissionViewSet(OrgBulkModelViewSet):
"""
应用授权列表的增删改查API
"""
model = ApplicationPermission
serializer_class = serializers.ApplicationPermissionSerializer
filter_fields = ['name', 'category', 'type']
search_fields = filter_fields
permission_classes = (IsOrgAdmin,)
def get_queryset(self):
queryset = super().get_queryset().prefetch_related(
"applications", "users", "user_groups", "system_users"
)
return queryset

View File

@ -0,0 +1,128 @@
# -*- 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 common.permissions import IsOrgAdmin
from perms import serializers
from perms import models
__all__ = [
'ApplicationPermissionUserRelationViewSet',
'ApplicationPermissionUserGroupRelationViewSet',
'ApplicationPermissionApplicationRelationViewSet',
'ApplicationPermissionSystemUserRelationViewSet',
'ApplicationPermissionAllApplicationListApi',
'ApplicationPermissionAllUserListApi',
]
class RelationMixin(OrgRelationMixin, OrgBulkModelViewSet):
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
permission_classes = (IsOrgAdmin,)
filter_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
permission_classes = (IsOrgAdmin,)
filter_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
permission_classes = (IsOrgAdmin,)
filter_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
permission_classes = (IsOrgAdmin,)
filter_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):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.ApplicationPermissionAllApplicationSerializer
only_fields = serializers.ApplicationPermissionAllApplicationSerializer.Meta.only_fields
filter_fields = ('name',)
search_fields = filter_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):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.ApplicationPermissionAllUserSerializer
only_fields = serializers.ApplicationPermissionAllUserSerializer.Meta.only_fields
filter_fields = ('username', 'name')
search_fields = filter_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

View File

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
#
from django.db.models import Q
from rest_framework.generics import ListAPIView
from common.permissions import IsOrgAdminOrAppUser
from applications.models import Application
from applications.api.mixin import ApplicationAttrsSerializerViewMixin
from perms import serializers
__all__ = [
'UserGroupGrantedApplicationsApi'
]
class UserGroupGrantedApplicationsApi(ApplicationAttrsSerializerViewMixin, ListAPIView):
"""
获取用户组直接授权的资产
"""
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.ApplicationGrantedSerializer
only_fields = serializers.ApplicationGrantedSerializer.Meta.only_fields
filter_fields = ['id', 'name', 'category', 'type', 'comment']
search_fields = ['name', 'comment']
def get_queryset(self):
user_group_id = self.kwargs.get('pk', '')
queryset = Application.objects\
.filter(Q(granted_by_permissions__user_groups__id=user_group_id))\
.distinct().only(*self.only_fields)
return queryset

View File

@ -0,0 +1,2 @@
from .user_permission_applications import *
from .common import *

View File

@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
#
import uuid
from django.shortcuts import get_object_or_404
from rest_framework.views import APIView, Response
from rest_framework.generics import (
ListAPIView, get_object_or_404
)
from applications.models import Application
from perms.utils.application.permission import (
get_application_system_users_id
)
from perms.api.asset.user_permission.mixin import ForAdminMixin, ForUserMixin
from common.permissions import IsOrgAdminOrAppUser
from perms.hands import User, SystemUser
from perms import serializers
__all__ = [
'UserGrantedApplicationSystemUsersApi',
'MyGrantedApplicationSystemUsersApi',
'ValidateUserApplicationPermissionApi'
]
class GrantedApplicationSystemUsersMixin(ListAPIView):
serializer_class = serializers.ApplicationSystemUserSerializer
only_fields = serializers.ApplicationSystemUserSerializer.Meta.only_fields
user: None
def get_application_system_users_id(self, application):
return get_application_system_users_id(self.user, application)
def get_queryset(self):
application_id = self.kwargs.get('application_id')
application = get_object_or_404(Application, id=application_id)
system_users_id = self.get_application_system_users_id(application)
system_users = SystemUser.objects.filter(id__in=system_users_id)\
.only(*self.only_fields).order_by('priority')
return system_users
class UserGrantedApplicationSystemUsersApi(ForAdminMixin, GrantedApplicationSystemUsersMixin):
pass
class MyGrantedApplicationSystemUsersApi(ForUserMixin, GrantedApplicationSystemUsersMixin):
pass
class ValidateUserApplicationPermissionApi(APIView):
permission_classes = (IsOrgAdminOrAppUser,)
def get(self, request, *args, **kwargs):
user_id = request.query_params.get('user_id', '')
application_id = request.query_params.get('application_id', '')
system_user_id = request.query_params.get('system_user_id', '')
try:
user_id = uuid.UUID(user_id)
application_id = uuid.UUID(application_id)
system_user_id = uuid.UUID(system_user_id)
except ValueError:
return Response({'msg': False}, status=403)
user = get_object_or_404(User, id=user_id)
application = get_object_or_404(Application, id=application_id)
system_user = get_object_or_404(SystemUser, id=system_user_id)
system_users_id = get_application_system_users_id(user, application)
if system_user.id in system_users_id:
return Response({'msg': True}, status=200)
return Response({'msg': False}, status=403)

View File

@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
#
from rest_framework.generics import ListAPIView
from rest_framework.response import Response
from applications.api.mixin import (
SerializeApplicationToTreeNodeMixin, ApplicationAttrsSerializerViewMixin
)
from perms import serializers
from perms.api.asset.user_permission.mixin import ForAdminMixin, ForUserMixin
from perms.utils.application.user_permission import (
get_user_granted_all_applications
)
__all__ = [
'UserAllGrantedApplicationsApi',
'MyAllGrantedApplicationsApi',
'UserAllGrantedApplicationsAsTreeApi',
'MyAllGrantedApplicationsAsTreeApi',
]
class AllGrantedApplicationsMixin(ApplicationAttrsSerializerViewMixin, ListAPIView):
only_fields = serializers.ApplicationGrantedSerializer.Meta.only_fields
serializer_class = serializers.ApplicationGrantedSerializer
filter_fields = ['id', 'name', 'category', 'type', 'comment']
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(ForAdminMixin, AllGrantedApplicationsMixin):
pass
class MyAllGrantedApplicationsApi(ForUserMixin, AllGrantedApplicationsMixin):
pass
class ApplicationsAsTreeMixin(SerializeApplicationToTreeNodeMixin):
"""
将应用序列化成树的结构返回
"""
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
data = self.serialize_applications(queryset)
return Response(data=data)
class UserAllGrantedApplicationsAsTreeApi(ApplicationsAsTreeMixin, UserAllGrantedApplicationsApi):
pass
class MyAllGrantedApplicationsAsTreeApi(ApplicationsAsTreeMixin, MyAllGrantedApplicationsApi):
pass

View File

@ -0,0 +1,4 @@
from .user_permission import *
from .asset_permission import *
from .asset_permission_relation import *
from .user_group_permission import *

View File

@ -3,13 +3,13 @@
from django.db.models import Q from django.db.models import Q
from common.permissions import IsOrgAdmin from common.permissions import IsOrgAdmin
from orgs.mixins.api import OrgModelViewSet from orgs.mixins.api import OrgBulkModelViewSet
from common.utils import get_object_or_none from common.utils import get_object_or_none
from ..models import AssetPermission from perms.models import AssetPermission
from ..hands import ( from perms.hands import (
User, UserGroup, Asset, Node, SystemUser, User, UserGroup, Asset, Node, SystemUser,
) )
from .. import serializers from perms import serializers
__all__ = [ __all__ = [
@ -17,7 +17,7 @@ __all__ = [
] ]
class AssetPermissionViewSet(OrgModelViewSet): class AssetPermissionViewSet(OrgBulkModelViewSet):
""" """
资产授权列表的增删改查api 资产授权列表的增删改查api
""" """

View File

@ -11,8 +11,8 @@ from orgs.mixins.api import OrgRelationMixin
from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins.api import OrgBulkModelViewSet
from orgs.utils import current_org from orgs.utils import current_org
from common.permissions import IsOrgAdmin from common.permissions import IsOrgAdmin
from .. import serializers from perms import serializers
from .. import models from perms import models
__all__ = [ __all__ = [
'AssetPermissionUserRelationViewSet', 'AssetPermissionUserGroupRelationViewSet', 'AssetPermissionUserRelationViewSet', 'AssetPermissionUserGroupRelationViewSet',

View File

@ -10,9 +10,9 @@ from common.permissions import IsOrgAdminOrAppUser
from common.utils import lazyproperty from common.utils import lazyproperty
from perms.models import AssetPermission from perms.models import AssetPermission
from assets.models import Asset, Node from assets.models import Asset, Node
from . import user_permission as uapi from perms.api.asset import user_permission as uapi
from perms import serializers from perms import serializers
from perms.utils.asset_permission import get_asset_system_users_id_with_actions_by_group from perms.utils.asset.permission import get_asset_system_users_id_with_actions_by_group
from assets.api.mixin import SerializeToTreeNodeMixin from assets.api.mixin import SerializeToTreeNodeMixin
from users.models import UserGroup from users.models import UserGroup

View File

@ -10,13 +10,13 @@ from rest_framework.generics import (
) )
from orgs.utils import tmp_to_root_org from orgs.utils import tmp_to_root_org
from perms.utils.asset_permission import get_asset_system_users_id_with_actions_by_user from perms.utils.asset.permission import get_asset_system_users_id_with_actions_by_user
from common.permissions import IsOrgAdminOrAppUser, IsOrgAdmin, IsValidUser from common.permissions import IsOrgAdminOrAppUser, IsOrgAdmin, IsValidUser
from common.utils import get_logger, lazyproperty from common.utils import get_logger, lazyproperty
from ...hands import User, Asset, SystemUser from perms.hands import User, Asset, SystemUser
from ... import serializers from perms import serializers
from ...models import Action from perms.models import Action
logger = get_logger(__name__) logger = get_logger(__name__)

View File

@ -4,6 +4,7 @@ from rest_framework.request import Request
from common.permissions import IsOrgAdminOrAppUser, IsValidUser from common.permissions import IsOrgAdminOrAppUser, IsValidUser
from common.utils import lazyproperty from common.utils import lazyproperty
from orgs.utils import tmp_to_root_org
from users.models import User from users.models import User
from perms.models import UserGrantedMappingNode from perms.models import UserGrantedMappingNode
@ -47,6 +48,10 @@ class ForUserMixin:
permission_classes = (IsValidUser,) permission_classes = (IsValidUser,)
request: Request request: Request
def get(self, request, *args, **kwargs):
with tmp_to_root_org():
return super().get(request, *args, **kwargs)
@lazyproperty @lazyproperty
def user(self): def user(self):
return self.request.user return self.request.user

View File

@ -1,7 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.utils.decorators import method_decorator from perms.api.asset.user_permission.mixin import UserNodeGrantStatusDispatchMixin
from perms.api.user_permission.mixin import UserNodeGrantStatusDispatchMixin
from rest_framework.generics import ListAPIView from rest_framework.generics import ListAPIView
from rest_framework.response import Response from rest_framework.response import Response
from django.conf import settings from django.conf import settings
@ -10,9 +9,8 @@ from assets.api.mixin import SerializeToTreeNodeMixin
from common.utils import get_logger from common.utils import get_logger
from perms.pagination import GrantedAssetLimitOffsetPagination from perms.pagination import GrantedAssetLimitOffsetPagination
from assets.models import Asset, Node, FavoriteAsset from assets.models import Asset, Node, FavoriteAsset
from orgs.utils import tmp_to_root_org from perms import serializers
from ... import serializers from perms.utils.asset.user_permission import (
from ...utils.user_asset_permission import (
get_node_all_granted_assets, get_user_direct_granted_assets, get_node_all_granted_assets, get_user_direct_granted_assets,
get_user_granted_all_assets get_user_granted_all_assets
) )
@ -22,7 +20,6 @@ from .mixin import ForAdminMixin, ForUserMixin
logger = get_logger(__name__) logger = get_logger(__name__)
@method_decorator(tmp_to_root_org(), name='list')
class UserDirectGrantedAssetsApi(ListAPIView): class UserDirectGrantedAssetsApi(ListAPIView):
""" """
用户直接授权的资产的列表也就是授权规则上直接授权的资产并非是来自节点的 用户直接授权的资产的列表也就是授权规则上直接授权的资产并非是来自节点的
@ -40,7 +37,6 @@ class UserDirectGrantedAssetsApi(ListAPIView):
return assets return assets
@method_decorator(tmp_to_root_org(), name='list')
class UserFavoriteGrantedAssetsApi(ListAPIView): class UserFavoriteGrantedAssetsApi(ListAPIView):
serializer_class = serializers.AssetGrantedSerializer serializer_class = serializers.AssetGrantedSerializer
only_fields = serializers.AssetGrantedSerializer.Meta.only_fields only_fields = serializers.AssetGrantedSerializer.Meta.only_fields
@ -55,7 +51,6 @@ class UserFavoriteGrantedAssetsApi(ListAPIView):
return assets return assets
@method_decorator(tmp_to_root_org(), name='list')
class AssetsAsTreeMixin(SerializeToTreeNodeMixin): class AssetsAsTreeMixin(SerializeToTreeNodeMixin):
""" """
资产 序列化成树的结构返回 资产 序列化成树的结构返回
@ -82,12 +77,10 @@ class MyFavoriteGrantedAssetsApi(ForUserMixin, UserFavoriteGrantedAssetsApi):
pass pass
@method_decorator(tmp_to_root_org(), name='list')
class UserDirectGrantedAssetsAsTreeForAdminApi(ForAdminMixin, AssetsAsTreeMixin, UserDirectGrantedAssetsApi): class UserDirectGrantedAssetsAsTreeForAdminApi(ForAdminMixin, AssetsAsTreeMixin, UserDirectGrantedAssetsApi):
pass pass
@method_decorator(tmp_to_root_org(), name='list')
class MyUngroupAssetsAsTreeApi(ForUserMixin, AssetsAsTreeMixin, UserDirectGrantedAssetsApi): class MyUngroupAssetsAsTreeApi(ForUserMixin, AssetsAsTreeMixin, UserDirectGrantedAssetsApi):
def get_queryset(self): def get_queryset(self):
queryset = super().get_queryset() queryset = super().get_queryset()
@ -96,9 +89,11 @@ class MyUngroupAssetsAsTreeApi(ForUserMixin, AssetsAsTreeMixin, UserDirectGrante
return queryset return queryset
@method_decorator(tmp_to_root_org(), name='list') class UserAllGrantedAssetsApi(ForAdminMixin, ListAPIView):
class UserAllGrantedAssetsApi(ListAPIView):
only_fields = serializers.AssetGrantedSerializer.Meta.only_fields only_fields = serializers.AssetGrantedSerializer.Meta.only_fields
serializer_class = serializers.AssetGrantedSerializer
filter_fields = ['hostname', 'ip', 'id', 'comment']
search_fields = ['hostname', 'ip', 'comment']
def get_queryset(self): def get_queryset(self):
queryset = get_user_granted_all_assets(self.user) queryset = get_user_granted_all_assets(self.user)
@ -106,11 +101,14 @@ class UserAllGrantedAssetsApi(ListAPIView):
return queryset.only(*self.only_fields) return queryset.only(*self.only_fields)
class MyAllGrantedAssetsApi(ForUserMixin, UserAllGrantedAssetsApi):
pass
class MyAllAssetsAsTreeApi(ForUserMixin, AssetsAsTreeMixin, UserAllGrantedAssetsApi): class MyAllAssetsAsTreeApi(ForUserMixin, AssetsAsTreeMixin, UserAllGrantedAssetsApi):
search_fields = ['hostname', 'ip'] search_fields = ['hostname', 'ip']
@method_decorator(tmp_to_root_org(), name='list')
class UserGrantedNodeAssetsApi(UserNodeGrantStatusDispatchMixin, ListAPIView): class UserGrantedNodeAssetsApi(UserNodeGrantStatusDispatchMixin, ListAPIView):
serializer_class = serializers.AssetGrantedSerializer serializer_class = serializers.AssetGrantedSerializer
only_fields = serializers.AssetGrantedSerializer.Meta.only_fields only_fields = serializers.AssetGrantedSerializer.Meta.only_fields

View File

@ -7,13 +7,12 @@ from rest_framework.generics import (
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.request import Request from rest_framework.request import Request
from orgs.utils import tmp_to_root_org
from assets.api.mixin import SerializeToTreeNodeMixin from assets.api.mixin import SerializeToTreeNodeMixin
from common.utils import get_logger from common.utils import get_logger
from .mixin import ForAdminMixin, ForUserMixin, UserNodeGrantStatusDispatchMixin from .mixin import ForAdminMixin, ForUserMixin, UserNodeGrantStatusDispatchMixin
from ...hands import Node, User from perms.hands import Node, User
from ... import serializers from perms import serializers
from ...utils.user_asset_permission import ( from perms.utils.asset.user_permission import (
get_indirect_granted_node_children, get_indirect_granted_node_children,
get_user_granted_nodes_list_via_mapping_node, get_user_granted_nodes_list_via_mapping_node,
get_top_level_granted_nodes, get_top_level_granted_nodes,
@ -59,7 +58,6 @@ class NodeChildrenMixin:
class BaseGrantedNodeApi(_GrantedNodeStructApi, metaclass=abc.ABCMeta): class BaseGrantedNodeApi(_GrantedNodeStructApi, metaclass=abc.ABCMeta):
serializer_class = serializers.NodeGrantedSerializer serializer_class = serializers.NodeGrantedSerializer
@tmp_to_root_org()
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
rebuild_user_tree_if_need(request, self.user) rebuild_user_tree_if_need(request, self.user)
nodes = self.get_nodes() nodes = self.get_nodes()
@ -72,7 +70,6 @@ class BaseNodeChildrenApi(NodeChildrenMixin, BaseGrantedNodeApi, metaclass=abc.A
class BaseGrantedNodeAsTreeApi(SerializeToTreeNodeMixin, _GrantedNodeStructApi, metaclass=abc.ABCMeta): class BaseGrantedNodeAsTreeApi(SerializeToTreeNodeMixin, _GrantedNodeStructApi, metaclass=abc.ABCMeta):
@tmp_to_root_org()
def list(self, request: Request, *args, **kwargs): def list(self, request: Request, *args, **kwargs):
rebuild_user_tree_if_need(request, self.user) rebuild_user_tree_if_need(request, self.user)
nodes = self.get_nodes() nodes = self.get_nodes()

View File

@ -5,22 +5,21 @@ from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from django.db.models import F from django.db.models import F
from orgs.utils import tmp_to_root_org
from common.permissions import IsValidUser from common.permissions import IsValidUser
from common.utils import get_logger, get_object_or_none from common.utils import get_logger, get_object_or_none
from .mixin import UserNodeGrantStatusDispatchMixin, ForUserMixin, ForAdminMixin from .mixin import UserNodeGrantStatusDispatchMixin, ForUserMixin, ForAdminMixin
from ...utils.user_asset_permission import ( from perms.utils.asset.user_permission import (
get_user_resources_q_granted_by_permissions,
get_indirect_granted_node_children, UNGROUPED_NODE_KEY, FAVORITE_NODE_KEY, get_indirect_granted_node_children, UNGROUPED_NODE_KEY, FAVORITE_NODE_KEY,
get_user_direct_granted_assets, get_top_level_granted_nodes, get_user_direct_granted_assets, get_top_level_granted_nodes,
get_user_granted_nodes_list_via_mapping_node, get_user_granted_nodes_list_via_mapping_node,
get_user_granted_all_assets, rebuild_user_tree_if_need, get_user_granted_all_assets, rebuild_user_tree_if_need,
get_user_all_assetpermission_ids, get_user_all_assetpermissions_id,
) )
from assets.models import Asset, FavoriteAsset from assets.models import Asset, FavoriteAsset
from assets.api import SerializeToTreeNodeMixin from assets.api import SerializeToTreeNodeMixin
from orgs.utils import tmp_to_root_org from perms.hands import Node
from ...hands import Node
logger = get_logger(__name__) logger = get_logger(__name__)
@ -66,14 +65,14 @@ class UserGrantedNodeChildrenWithAssetsAsTreeForAdminApi(ForAdminMixin, UserNode
def get_data_on_node_indirect_granted(self, key): def get_data_on_node_indirect_granted(self, key):
user = self.user user = self.user
asset_perm_ids = get_user_all_assetpermission_ids(user) asset_perms_id = get_user_all_assetpermissions_id(user)
nodes = get_indirect_granted_node_children(user, key) nodes = get_indirect_granted_node_children(user, key)
assets = Asset.org_objects.filter( assets = Asset.org_objects.filter(
nodes__key=key, nodes__key=key,
).filter( ).filter(
granted_by_permissions__id__in=asset_perm_ids granted_by_permissions__id__in=asset_perms_id
).distinct() ).distinct()
assets = assets.prefetch_related('platform') assets = assets.prefetch_related('platform')
return nodes, assets return nodes, assets
@ -102,7 +101,6 @@ class UserGrantedNodeChildrenWithAssetsAsTreeForAdminApi(ForAdminMixin, UserNode
if node: if node:
return node.key return node.key
@tmp_to_root_org()
def list(self, request: Request, *args, **kwargs): def list(self, request: Request, *args, **kwargs):
key = self.request.query_params.get('key') key = self.request.query_params.get('key')
if key is None: if key is None:

View File

@ -60,7 +60,7 @@ class AssetPermissionForm(OrgModelForm):
# 过滤系统用户 # 过滤系统用户
system_users_field = self.fields.get('system_users') system_users_field = self.fields.get('system_users')
system_users_field.queryset = SystemUser.objects.exclude( system_users_field.queryset = SystemUser.objects.exclude(
protocol=SystemUser.PROTOCOL_MYSQL protocol__in=SystemUser.application_category_protocols
) )
def set_nodes_initial(self, nodes): def set_nodes_initial(self, nodes):

View File

@ -25,7 +25,7 @@ class DatabaseAppPermissionCreateUpdateForm(OrgModelForm):
# 过滤系统用户 # 过滤系统用户
system_users_field = self.fields.get('system_users') system_users_field = self.fields.get('system_users')
system_users_field.queryset = SystemUser.objects.filter( system_users_field.queryset = SystemUser.objects.filter(
protocol=SystemUser.PROTOCOL_MYSQL protocol__in=SystemUser.application_category_protocols
) )
class Meta: class Meta:

View File

@ -0,0 +1,237 @@
# Generated by Django 2.2.13 on 2020-10-28 02:19
import common.utils.django
from django.conf import settings
from django.db import migrations, models
import django.utils.timezone
import uuid
def old_perm_to_application_permission_json(old_perm, category, _type):
return {
'id': old_perm.id,
'name': old_perm.name,
'category': category,
'type': _type,
'is_active': old_perm.is_active,
'date_start': old_perm.date_start,
'date_expired': old_perm.date_expired,
'created_by': old_perm.created_by,
'comment': old_perm.comment,
'org_id': old_perm.org_id
}
def common_old_perm_relation_to_application_permission_json(old_perm_relation, data_json):
return {
'applicationpermission_id': getattr(old_perm_relation, data_json['relation_app_perm_id']),
}
def old_perm_relation_app_to_application_permission_json(old_perm_relation_app, data_json):
data = common_old_perm_relation_to_application_permission_json(old_perm_relation_app, data_json)
data.update({
'application_id': getattr(old_perm_relation_app, data_json['relation_app_id'])
})
return data
def old_perm_relation_system_user_to_application_permission_json(old_perm_relation_system_user, data_json):
data = common_old_perm_relation_to_application_permission_json(old_perm_relation_system_user, data_json)
data.update({
'systemuser_id': old_perm_relation_system_user.systemuser_id,
})
return data
def old_perm_relation_user_group_to_application_permission_json(old_perm_relation_user_group, data_json):
data = common_old_perm_relation_to_application_permission_json(old_perm_relation_user_group, data_json)
data.update({
'usergroup_id': old_perm_relation_user_group.usergroup_id,
})
return data
def old_perm_relation_user_to_application_permission_json(old_perm_relation_user, data_json):
data = common_old_perm_relation_to_application_permission_json(old_perm_relation_user, data_json)
data.update({
'user_id': old_perm_relation_user.user_id,
})
return data
CATEGORY_DB = 'db'
CATEGORY_REMOTE = 'remote_app'
CATEGORY_CLOUD = 'cloud'
TYPE_DB_MYSQL = 'mysql'
TYPE_CLOUD_K8S = 'k8s'
TYPE_REMOTE_CHROME = 'chrome'
TYPE_REMOTE_MYSQL_WORKBENCH = 'mysql_workbench'
TYPE_REMOTE_VMWARE_CLIENT = 'vmware_client'
TYPE_REMOTE_CUSTOM = 'custom'
OLD_PERM_MODELS_NAME_MAP_DATA_JSON = {
'DatabaseAppPermission': {
'app_m2m_fields': 'database_apps',
'relation_app_perm_id': 'databaseapppermission_id',
'relation_app_id': 'databaseapp_id',
'category': CATEGORY_DB,
'type': TYPE_DB_MYSQL,
},
'RemoteAppPermission': {
'app_m2m_fields': 'remote_apps',
'relation_app_perm_id': 'remoteapppermission_id',
'relation_app_id': 'remoteapp_id',
'category': CATEGORY_REMOTE,
'type': None,
},
'K8sAppPermission': {
'app_m2m_fields': 'k8s_apps',
'relation_app_perm_id': 'k8sapppermission_id',
'relation_app_id': 'k8sapp_id',
'category': CATEGORY_CLOUD,
'type': TYPE_CLOUD_K8S,
}
}
def migrate_and_integrate_application_permissions(apps, schema_editor):
db_alias = schema_editor.connection.alias
new_app_perms_json = []
new_app_perms_relation_apps_json = []
new_app_perms_relation_system_users_json = []
new_app_perms_relation_user_groups_json = []
new_app_perms_relation_users_json = []
for old_perm_model_name, data_json in OLD_PERM_MODELS_NAME_MAP_DATA_JSON.items():
# model
old_perm_model = apps.get_model("perms", old_perm_model_name)
# instances
old_perms = old_perm_model.objects.using(db_alias).all()
old_perms_relation_apps = getattr(old_perm_model, data_json['app_m2m_fields']).through.objects.using(db_alias).all()
old_perms_relation_system_users = old_perm_model.system_users.through.objects.using(db_alias).all()
old_perms_relation_user_groups = old_perm_model.user_groups.through.objects.using(db_alias).all()
old_perms_relation_users = old_perm_model.users.through.objects.using(db_alias).all()
# json
perms_json = []
category = data_json['category']
for old_perm in old_perms:
if category == CATEGORY_REMOTE:
type_list = list(old_perm.remote_apps.values_list('type', flat=True))
if len(type_list) == 0:
_type = TYPE_REMOTE_CHROME
else:
_type = max(type_list, key=type_list.count)
else:
_type = data_json['type']
perm_json = old_perm_to_application_permission_json(old_perm, category, _type)
perms_json.append(perm_json)
perms_relation_apps_json = [
old_perm_relation_app_to_application_permission_json(old_perm_relation_app, data_json)
for old_perm_relation_app in old_perms_relation_apps
]
perms_relation_system_users_json = [
old_perm_relation_system_user_to_application_permission_json(old_perm_relation_system_user, data_json)
for old_perm_relation_system_user in old_perms_relation_system_users
]
perms_relation_user_groups_json = [
old_perm_relation_user_group_to_application_permission_json(old_perm_relation_user_group, data_json)
for old_perm_relation_user_group in old_perms_relation_user_groups
]
perms_relation_users_json = [
old_perm_relation_user_to_application_permission_json(old_perm_relation_user, data_json)
for old_perm_relation_user in old_perms_relation_users
]
new_app_perms_json.extend(perms_json)
new_app_perms_relation_apps_json.extend(perms_relation_apps_json)
new_app_perms_relation_system_users_json.extend(perms_relation_system_users_json)
new_app_perms_relation_user_groups_json.extend(perms_relation_user_groups_json)
new_app_perms_relation_users_json.extend(perms_relation_users_json)
# model
new_app_perm_model = apps.get_model("perms", "ApplicationPermission")
new_app_perm_relation_app_model = new_app_perm_model.applications.through
new_app_perm_relation_system_user_model = new_app_perm_model.system_users.through
new_app_perm_relation_user_group_model = new_app_perm_model.user_groups.through
new_app_perm_relation_user_model = new_app_perm_model.users.through
# instances
new_app_perm_objects = [
new_app_perm_model(**data)
for data in new_app_perms_json
]
new_app_perm_relation_app_objects = [
new_app_perm_relation_app_model(**data)
for data in new_app_perms_relation_apps_json
]
new_app_perm_relation_system_user_objects = [
new_app_perm_relation_system_user_model(**data)
for data in new_app_perms_relation_system_users_json
]
new_app_perm_relation_user_group_objects = [
new_app_perm_relation_user_group_model(**data)
for data in new_app_perms_relation_user_groups_json
]
new_app_perm_relation_user_objects = [
new_app_perm_relation_user_model(**data)
for data in new_app_perms_relation_users_json
]
# create
for new_app_perm_object in new_app_perm_objects:
if new_app_perm_model.objects.using(db_alias).filter(name=new_app_perm_object.name).exists():
new_app_perm_object.name = '{}-{}'.format(new_app_perm_object.name, str(new_app_perm_object.id)[:4])
new_app_perm_object.save()
new_app_perm_relation_app_model.objects.using(db_alias).bulk_create(new_app_perm_relation_app_objects)
new_app_perm_relation_system_user_model.objects.using(db_alias).bulk_create(new_app_perm_relation_system_user_objects)
new_app_perm_relation_user_group_model.objects.using(db_alias).bulk_create(new_app_perm_relation_user_group_objects)
new_app_perm_relation_user_model.objects.using(db_alias).bulk_create(new_app_perm_relation_user_objects)
class Migration(migrations.Migration):
dependencies = [
('users', '0030_auto_20200819_2041'),
('assets', '0059_auto_20201027_1905'),
('applications', '0006_application'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('perms', '0015_auto_20200929_1728'),
]
operations = [
migrations.CreateModel(
name='ApplicationPermission',
fields=[
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, verbose_name='Name')),
('is_active', models.BooleanField(default=True, verbose_name='Active')),
('date_start', models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='Date start')),
('date_expired', models.DateTimeField(db_index=True, default=common.utils.django.date_expired_default, verbose_name='Date expired')),
('created_by', models.CharField(blank=True, max_length=128, verbose_name='Created by')),
('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')),
('comment', models.TextField(blank=True, verbose_name='Comment')),
('category', models.CharField(choices=[('db', 'Database'), ('remote_app', 'Remote app'), ('cloud', 'Cloud')], max_length=16, verbose_name='Category')),
('type', models.CharField(choices=[('mysql', 'MySQL'), ('oracle', 'Oracle'), ('postgresql', 'PostgreSQL'), ('mariadb', 'MariaDB'), ('chrome', 'Chrome'), ('mysql_workbench', 'MySQL Workbench'), ('vmware_client', 'vSphere Client'), ('custom', 'Custom'), ('k8s', 'Kubernetes')], max_length=16, verbose_name='Type')),
('applications', models.ManyToManyField(blank=True, related_name='granted_by_permissions', to='applications.Application', verbose_name='Application')),
('system_users', models.ManyToManyField(related_name='granted_by_application_permissions', to='assets.SystemUser', verbose_name='System user')),
('user_groups', models.ManyToManyField(blank=True, related_name='applicationpermissions', to='users.UserGroup', verbose_name='User group')),
('users', models.ManyToManyField(blank=True, related_name='applicationpermissions', to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'verbose_name': 'Application permission',
'ordering': ('name',),
'unique_together': {('org_id', 'name')},
},
),
migrations.RunPython(migrate_and_integrate_application_permissions),
]

View File

@ -2,6 +2,7 @@
# #
from .asset_permission import * from .asset_permission import *
from .application_permission import *
from .remote_app_permission import * from .remote_app_permission import *
from .database_app_permission import * from .database_app_permission import *
from .k8s_app_permission import * from .k8s_app_permission import *

View File

@ -0,0 +1,51 @@
# coding: utf-8
#
from django.db import models
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from common.utils import lazyproperty
from .base import BasePermission
from users.models import User
from applications.models import Category
__all__ = [
'ApplicationPermission',
]
class ApplicationPermission(BasePermission):
category = models.CharField(max_length=16, choices=Category.choices, verbose_name=_('Category'))
type = models.CharField(max_length=16, choices=Category.get_all_type_choices(), verbose_name=_('Type'))
applications = models.ManyToManyField('applications.Application', related_name='granted_by_permissions', blank=True, verbose_name=_("Application"))
system_users = models.ManyToManyField('assets.SystemUser', related_name='granted_by_application_permissions', verbose_name=_("System user"))
class Meta:
unique_together = [('org_id', 'name')]
verbose_name = _('Application permission')
ordering = ('name',)
@lazyproperty
def users_amount(self):
return self.users.count()
@lazyproperty
def user_groups_amount(self):
return self.user_groups.count()
@lazyproperty
def applications_amount(self):
return self.applications.count()
@lazyproperty
def system_users_amount(self):
return self.system_users.count()
def get_all_users(self):
users_id = self.users.all().values_list('id', flat=True)
user_groups_id = self.user_groups.all().values_list('id', flat=True)
users = User.objects.filter(
Q(id__in=users_id) | Q(groups__id__in=user_groups_id)
)
return users

View File

@ -1,11 +1,12 @@
# coding: utf-8 # coding: utf-8
# #
from .asset import *
from .application import *
from .system_user_permission import * from .system_user_permission import *
from .asset_permission import *
from .user_permission import * # TODO: 删除
from .remote_app_permission import * from .remote_app_permission import *
from .remote_app_permission_relation import * from .remote_app_permission_relation import *
from .asset_permission_relation import *
from .database_app_permission import * from .database_app_permission import *
from .database_app_permission_relation import * from .database_app_permission_relation import *
from .base import * from .base import *

View File

@ -0,0 +1,3 @@
from .permission import *
from .permission_relation import *
from .user_permission import *

View File

@ -0,0 +1,57 @@
# -*- 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
__all__ = [
'ApplicationPermissionSerializer'
]
class ApplicationPermissionSerializer(BulkOrgResourceModelSerializer):
category_display = serializers.ReadOnlyField(source='get_category_display', label=_('Category'))
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type'))
is_valid = serializers.BooleanField(read_only=True)
is_expired = serializers.BooleanField(read_only=True)
class Meta:
model = ApplicationPermission
mini_fields = ['id', 'name']
small_fields = mini_fields + [
'category', 'category_display', 'type', 'type_display', 'is_active', 'is_expired',
'is_valid', 'created_by', 'date_created', 'date_expired', 'date_start', 'comment'
]
m2m_fields = [
'users', 'user_groups', 'applications', 'system_users',
'users_amount', 'user_groups_amount', 'applications_amount', 'system_users_amount',
]
fields = small_fields + m2m_fields
read_only_fields = ['created_by', 'date_created']
@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

View File

@ -0,0 +1,92 @@
# -*- coding: utf-8 -*-
#
from rest_framework import serializers
from common.mixins import BulkSerializerMixin
from common.serializers import AdaptedBulkListSerializer
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 Meta:
list_serializer_class = AdaptedBulkListSerializer
class ApplicationPermissionUserRelationSerializer(RelationMixin, serializers.ModelSerializer):
user_display = serializers.ReadOnlyField()
class Meta(RelationMixin.Meta):
model = ApplicationPermission.users.through
fields = [
'id', 'user', 'user_display',
]
class ApplicationPermissionUserGroupRelationSerializer(RelationMixin, serializers.ModelSerializer):
usergroup_display = serializers.ReadOnlyField()
class Meta(RelationMixin.Meta):
model = ApplicationPermission.user_groups.through
fields = [
'id', 'usergroup', "usergroup_display",
]
class ApplicationPermissionApplicationRelationSerializer(RelationMixin, serializers.ModelSerializer):
application_display = serializers.ReadOnlyField()
class Meta(RelationMixin.Meta):
model = ApplicationPermission.applications.through
fields = [
'id', "application", "application_display",
]
class ApplicationPermissionSystemUserRelationSerializer(RelationMixin, serializers.ModelSerializer):
systemuser_display = serializers.ReadOnlyField()
class Meta(RelationMixin.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)

View File

@ -0,0 +1,42 @@
# -*- 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
__all__ = [
'ApplicationGrantedSerializer',
'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 ApplicationGrantedSerializer(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

View File

@ -0,0 +1,3 @@
from .permission import *
from .permission_relation import *
from .user_permission import *

Some files were not shown because too many files have changed in this diff Show More