mirror of
https://github.com/jumpserver/jumpserver.git
synced 2026-07-02 07:01:30 +00:00
feat(assets): add user custom favorite folders (#16949)
* feat(assets): add user custom favorite folders - Add FavoriteFolder model (per-user, nested via self parent FK) - Add nullable folder FK on FavoriteAsset, allow one asset in multiple folders - Add FavoriteFolderViewSet and folder filter on FavoriteAssetViewSet - Serializer outputs asset_info for building connectable tree nodes - Register favorite-folders route; exclude favoritefolder from rbac/audit * feat(assets): register favorite-folders route and add migration - Register favorite-folders router endpoint - Add migration 0020 creating FavoriteFolder and folder FK on FavoriteAsset * feat(rbac): exclude favoritefolder from permission management Keep favoritefolder consistent with favoriteasset (user-owned, not RBAC managed) * feat(audits): exclude FavoriteFolder from operate log
This commit is contained in:
@@ -4,16 +4,15 @@ from rest_framework_bulk.generics import BulkModelViewSet
|
||||
|
||||
from common.permissions import IsValidUser
|
||||
from orgs.utils import tmp_to_root_org
|
||||
from ..models import FavoriteAsset
|
||||
from ..serializers import FavoriteAssetSerializer
|
||||
from ..models import FavoriteAsset, FavoriteFolder
|
||||
from ..serializers import FavoriteAssetSerializer, FavoriteFolderSerializer
|
||||
|
||||
__all__ = ['FavoriteAssetViewSet']
|
||||
__all__ = ['FavoriteAssetViewSet', 'FavoriteFolderViewSet']
|
||||
|
||||
|
||||
class FavoriteAssetViewSet(BulkModelViewSet):
|
||||
serializer_class = FavoriteAssetSerializer
|
||||
class FavoriteFolderViewSet(BulkModelViewSet):
|
||||
serializer_class = FavoriteFolderSerializer
|
||||
permission_classes = (IsValidUser,)
|
||||
filterset_fields = ['asset']
|
||||
page_no_limit = True
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
@@ -21,7 +20,23 @@ class FavoriteAssetViewSet(BulkModelViewSet):
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = FavoriteAsset.objects.filter(user=self.request.user)
|
||||
return FavoriteFolder.objects.filter(user=self.request.user)
|
||||
|
||||
|
||||
class FavoriteAssetViewSet(BulkModelViewSet):
|
||||
serializer_class = FavoriteAssetSerializer
|
||||
permission_classes = (IsValidUser,)
|
||||
filterset_fields = ['asset', 'folder']
|
||||
page_no_limit = True
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
with tmp_to_root_org():
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = FavoriteAsset.objects.filter(
|
||||
user=self.request.user
|
||||
).select_related('asset', 'asset__platform')
|
||||
return queryset
|
||||
|
||||
def allow_bulk_destroy(self, qs, filtered):
|
||||
|
||||
44
apps/assets/migrations/0020_favoritefolder.py
Normal file
44
apps/assets/migrations/0020_favoritefolder.py
Normal file
@@ -0,0 +1,44 @@
|
||||
# Generated for user custom favorite folders
|
||||
|
||||
import uuid
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('assets', '0019_alter_asset_connectivity'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='FavoriteFolder',
|
||||
fields=[
|
||||
('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')),
|
||||
('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated 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')),
|
||||
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='assets.favoritefolder')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Favorite folder',
|
||||
'unique_together': {('user', 'name', 'parent')},
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='favoriteasset',
|
||||
name='folder',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='assets', to='assets.favoritefolder'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='favoriteasset',
|
||||
unique_together={('user', 'asset', 'folder')},
|
||||
),
|
||||
]
|
||||
@@ -5,15 +5,36 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.db.models import JMSBaseModel
|
||||
|
||||
__all__ = ['FavoriteAsset']
|
||||
__all__ = ['FavoriteAsset', 'FavoriteFolder']
|
||||
|
||||
|
||||
class FavoriteFolder(JMSBaseModel):
|
||||
"""User custom favorite folder, owned by a user, visible across orgs, supports nesting"""
|
||||
user = models.ForeignKey('users.User', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=128, verbose_name=_("Name"))
|
||||
parent = models.ForeignKey(
|
||||
'self', on_delete=models.CASCADE,
|
||||
null=True, blank=True, related_name='children'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('user', 'name', 'parent')
|
||||
verbose_name = _("Favorite folder")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class FavoriteAsset(JMSBaseModel):
|
||||
user = models.ForeignKey('users.User', on_delete=models.CASCADE)
|
||||
asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE)
|
||||
folder = models.ForeignKey(
|
||||
'assets.FavoriteFolder', on_delete=models.CASCADE,
|
||||
null=True, blank=True, related_name='assets'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('user', 'asset')
|
||||
unique_together = ('user', 'asset', 'folder')
|
||||
verbose_name = _("Favorite asset")
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -4,16 +4,57 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.serializers import BulkSerializerMixin
|
||||
from ..models import FavoriteAsset
|
||||
from ..models import FavoriteAsset, FavoriteFolder
|
||||
|
||||
__all__ = ['FavoriteAssetSerializer']
|
||||
__all__ = ['FavoriteAssetSerializer', 'FavoriteFolderSerializer']
|
||||
|
||||
|
||||
class FavoriteFolderSerializer(BulkSerializerMixin, serializers.ModelSerializer):
|
||||
user = serializers.HiddenField(
|
||||
default=serializers.CurrentUserDefault()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = FavoriteFolder
|
||||
fields = ['id', 'user', 'name', 'parent', 'date_created']
|
||||
read_only_fields = ['id', 'date_created']
|
||||
|
||||
|
||||
class FavoriteAssetSerializer(BulkSerializerMixin, serializers.ModelSerializer):
|
||||
user = serializers.HiddenField(
|
||||
default=serializers.CurrentUserDefault()
|
||||
)
|
||||
asset_info = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = FavoriteAsset
|
||||
fields = ['user', 'asset']
|
||||
fields = ['user', 'asset', 'folder', 'asset_info']
|
||||
|
||||
@staticmethod
|
||||
def _get_icon(asset, platform):
|
||||
from assets.const import AllTypes
|
||||
support_types = AllTypes.get_types_values(exclude_custom=True)
|
||||
if asset.category == 'device':
|
||||
return 'switch'
|
||||
if asset.type in support_types:
|
||||
return asset.type
|
||||
return 'file'
|
||||
|
||||
def get_asset_info(self, obj):
|
||||
asset = obj.asset
|
||||
platform = asset.platform
|
||||
return {
|
||||
'id': str(asset.id),
|
||||
'name': asset.name,
|
||||
'iconSkin': self._get_icon(asset, platform),
|
||||
'chkDisabled': not asset.is_active,
|
||||
'meta': {
|
||||
'type': 'asset',
|
||||
'data': {
|
||||
'platform_type': platform.type,
|
||||
'org_name': asset.org_name,
|
||||
'name': asset.name,
|
||||
'address': asset.address,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ router.register(r'nodes', api.NodeViewSet, 'node')
|
||||
router.register(r'zones', api.ZoneViewSet, 'zone')
|
||||
router.register(r'gateways', api.GatewayViewSet, 'gateway')
|
||||
router.register(r'favorite-assets', api.FavoriteAssetViewSet, 'favorite-asset')
|
||||
router.register(r'favorite-folders', api.FavoriteFolderViewSet, 'favorite-folder')
|
||||
router.register(r'protocol-settings', api.PlatformProtocolViewSet, 'protocol-setting')
|
||||
router.register(r'labels', LabelViewSet, 'label')
|
||||
router.register(r'my-asset', api.MyAssetViewSet, 'my-asset')
|
||||
|
||||
@@ -80,18 +80,14 @@ def signal_of_operate_log_whether_continue(
|
||||
condition = False
|
||||
if instance and getattr(instance, OP_LOG_SKIP_SIGNAL, False):
|
||||
condition = False
|
||||
# 不记录组件的操作日志
|
||||
user = current_request.user if current_request else None
|
||||
if not user or getattr(user, 'is_service_account', False):
|
||||
condition = False
|
||||
# 终端模型的 create 事件由系统产生,不记录
|
||||
if instance._meta.object_name == 'Terminal' and created:
|
||||
condition = False
|
||||
# last_login 改变是最后登录日期, 每次登录都会改变
|
||||
if instance._meta.object_name == 'User' and \
|
||||
update_fields and 'last_login' in update_fields:
|
||||
condition = False
|
||||
# 不在记录白名单中,跳过
|
||||
if sender._meta.object_name not in MODELS_NEED_RECORD:
|
||||
condition = False
|
||||
return condition
|
||||
@@ -108,7 +104,6 @@ def on_object_pre_create_or_update(
|
||||
return
|
||||
|
||||
with translation.override('en'):
|
||||
# users.PrivateToken Model 没有 id 有 pk字段
|
||||
instance_id = getattr(instance, 'id', getattr(instance, 'pk', None))
|
||||
instance_before_data = {'id': instance_id}
|
||||
raw_instance = type(instance).objects.filter(pk=instance_id).first()
|
||||
@@ -188,7 +183,7 @@ def on_django_start_set_operate_log_monitor_models(sender, **kwargs):
|
||||
'PermedAsset', 'PermedAccount', 'MenuPermission',
|
||||
'Permission', 'TicketSession', 'ApplyLoginTicket',
|
||||
'ApplyCommandTicket', 'ApplyLoginAssetTicket',
|
||||
'FavoriteAsset', 'ChangeSecretRecord', 'AppProvider', 'Variable', 'LeakPasswords'
|
||||
'FavoriteAsset', 'FavoriteFolder', 'ChangeSecretRecord', 'AppProvider', 'Variable', 'LeakPasswords'
|
||||
}
|
||||
include_models = {'UserSession'}
|
||||
for i, app in enumerate(apps.get_models(), 1):
|
||||
|
||||
@@ -35,6 +35,7 @@ exclude_permissions = (
|
||||
('assets', 'cluster', '*', '*'),
|
||||
('assets', 'systemuser', '*', '*'),
|
||||
('assets', 'favoriteasset', '*', '*'),
|
||||
('assets', 'favoritefolder', '*', '*'),
|
||||
('assets', 'assetuser', '*', '*'),
|
||||
('assets', 'web', '*', '*'),
|
||||
('assets', 'host', '*', '*'),
|
||||
|
||||
Reference in New Issue
Block a user