Merge remote-tracking branch 'origin/v3' into v3

pull/9090/head
Aaron3S 2022-11-15 17:22:54 +08:00
commit b12f83dbea
26 changed files with 567 additions and 497 deletions

View File

@ -120,6 +120,13 @@ class BitChoicesField(TreeChoicesMixin, serializers.MultipleChoiceField):
super().__init__(choices=choices, **kwargs)
def to_representation(self, value):
if isinstance(value, list) and len(value) == 1:
# Swagger 会使用 field.choices.keys() 迭代传递进来
return [
{"value": c.name, "label": c.label}
for c in self._choice_cls
if c.name == value[0]
]
return [
{"value": c.name, "label": c.label}
for c in self._choice_cls

View File

@ -1,3 +1,5 @@
def bit(x):
return 2 ** (x - 1)
if x == 0:
return 0
else:
return 2 ** (x - 1)

View File

@ -1,53 +1,23 @@
from django.shortcuts import get_object_or_404
from rest_framework.generics import ListAPIView, get_object_or_404
from common.permissions import IsValidUser
from common.utils import get_logger, lazyproperty
from assets.serializers import AccountSerializer
from perms.hands import User, Asset, Account
from common.utils import get_logger
from perms import serializers
from perms.hands import Asset
from perms.utils import PermAccountUtil
from .mixin import RoleAdminMixin, RoleUserMixin
from .mixin import SelfOrPKUserMixin
logger = get_logger(__name__)
__all__ = [
'UserAllGrantedAccountsApi',
'MyAllGrantedAccountsApi',
'UserGrantedAssetAccountsApi',
'MyGrantedAssetAccountsApi',
'UserGrantedAssetSpecialAccountsApi',
'MyGrantedAssetSpecialAccountsApi',
]
class UserAllGrantedAccountsApi(RoleAdminMixin, ListAPIView):
""" 授权给用户的所有账号列表 """
serializer_class = AccountSerializer
filterset_fields = ("name", "username", "privileged", "version")
search_fields = filterset_fields
def get_queryset(self):
util = PermAccountUtil()
accounts = util.get_perm_accounts_for_user(self.user)
return accounts
class MyAllGrantedAccountsApi(RoleUserMixin, UserAllGrantedAccountsApi):
""" 授权给我的所有账号列表 """
pass
class UserGrantedAssetAccountsApi(ListAPIView):
class UserGrantedAssetAccountsApi(SelfOrPKUserMixin, ListAPIView):
serializer_class = serializers.AccountsGrantedSerializer
@lazyproperty
def user(self) -> User:
user_id = self.kwargs.get('pk')
return User.objects.get(id=user_id)
@lazyproperty
@property
def asset(self):
asset_id = self.kwargs.get('asset_id')
kwargs = {'id': asset_id, 'is_active': True}
@ -55,41 +25,6 @@ class UserGrantedAssetAccountsApi(ListAPIView):
return asset
def get_queryset(self):
accounts = PermAccountUtil().get_perm_accounts_for_user_asset(
self.user, self.asset, with_actions=True
)
util = PermAccountUtil()
accounts = util.get_permed_accounts_for_user(self.user, self.asset)
return accounts
class MyGrantedAssetAccountsApi(UserGrantedAssetAccountsApi):
permission_classes = (IsValidUser,)
@lazyproperty
def user(self):
return self.request.user
class UserGrantedAssetSpecialAccountsApi(ListAPIView):
serializer_class = serializers.AccountsGrantedSerializer
@lazyproperty
def user(self):
return self.request.user
def get_queryset(self):
# 构造默认包含的账号,如: @INPUT @USER
accounts = [
Account.get_manual_account(),
Account.get_user_account(self.user.username)
]
for account in accounts:
account.actions = Action.ALL
return accounts
class MyGrantedAssetSpecialAccountsApi(UserGrantedAssetSpecialAccountsApi):
permission_classes = (IsValidUser,)
@lazyproperty
def user(self):
return self.request.user

View File

@ -1,12 +1,12 @@
from rest_framework.generics import ListAPIView
from django.conf import settings
from rest_framework.generics import ListAPIView
from common.utils import get_logger
from ..mixin import AssetRoleAdminMixin, AssetRoleUserMixin
from .mixin import (
UserAllGrantedAssetsQuerysetMixin, UserDirectGrantedAssetsQuerysetMixin, UserFavoriteGrantedAssetsMixin,
UserGrantedNodeAssetsMixin, AssetsSerializerFormatMixin, AssetsTreeFormatMixin,
)
from ..mixin import AssetRoleAdminMixin, AssetRoleUserMixin
__all__ = [
'UserDirectGrantedAssetsApi', 'MyDirectGrantedAssetsApi',
@ -14,7 +14,8 @@ __all__ = [
'MyFavoriteGrantedAssetsApi', 'UserDirectGrantedAssetsAsTreeApi',
'MyUngroupAssetsAsTreeApi',
'UserAllGrantedAssetsApi', 'MyAllGrantedAssetsApi', 'MyAllAssetsAsTreeApi',
'UserGrantedNodeAssetsApi', 'MyGrantedNodeAssetsApi',
'UserGrantedNodeAssetsApi',
'MyGrantedNodeAssetsApi',
]
logger = get_logger(__name__)

View File

@ -1,10 +1,12 @@
# -*- coding: utf-8 -*-
#
from django.shortcuts import get_object_or_404
from rest_framework.request import Request
from common.http import is_true
from common.mixins.api import RoleAdminMixin, RoleUserMixin
from perms.utils.user_permission import UserGrantedTreeRefreshController
from rbac.permissions import RBACPermission
from users.models import User
@ -34,3 +36,43 @@ class AssetRoleUserMixin(RebuildTreeMixin, RoleUserMixin):
('get_tree', 'perms.view_myassets'),
('GET', 'perms.view_myassets'),
)
class SelfOrPKUserMixin:
kwargs: dict
request: Request
permission_classes = (RBACPermission,)
@property
def self_rbac_perms(self):
return (
('list', 'perms.view_myassets'),
('retrieve', 'perms.view_myassets'),
('get_tree', 'perms.view_myassets'),
('GET', 'perms.view_myassets'),
)
@property
def admin_rbac_perms(self):
return (
('list', 'perms.view_userassets'),
('retrieve', 'perms.view_userassets'),
('get_tree', 'perms.view_userassets'),
('GET', 'perms.view_userassets'),
)
def get_rbac_perms(self):
if self.request_user_is_self():
return self.self_rbac_perms
else:
return self.admin_rbac_perms
def request_user_is_self(self):
return self.kwargs.get('user') in ['my', 'self']
@property
def user(self):
if self.request_user_is_self():
return self.request.user
else:
return get_object_or_404(User, pk=self.kwargs.get('user'))

View File

@ -10,11 +10,11 @@ __all__ = ["SpecialAccount", "ActionChoices"]
class ActionChoices(BitChoices):
connect = bit(0), _("Connect")
upload = bit(1), _("Upload")
download = bit(2), _("Download")
copy = bit(3), _("Copy")
paste = bit(4), _("Paste")
connect = bit(1), _("Connect")
upload = bit(2), _("Upload")
download = bit(3), _("Download")
copy = bit(4), _("Copy")
paste = bit(5), _("Paste")
@classmethod
def is_tree(cls):

View File

@ -16,7 +16,7 @@ __all__ = ["AssetPermissionSerializer", "ActionChoicesField"]
class ActionChoicesField(BitChoicesField):
def __init__(self, **kwargs):
super().__init__(ActionChoices, **kwargs)
super().__init__(choice_cls=ActionChoices, **kwargs)
class AssetPermissionSerializer(BulkOrgResourceModelSerializer):

View File

@ -1,12 +1,12 @@
# -*- coding: utf-8 -*-
#
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from common.drf.fields import ObjectRelatedField, LabeledChoiceField
from assets.models import Node, Asset, Platform, Account
from assets.const import Category, AllTypes
from assets.models import Node, Asset, Platform, Account
from common.drf.fields import ObjectRelatedField, LabeledChoiceField
from perms.serializers.permission import ActionChoicesField
__all__ = [
@ -49,13 +49,9 @@ class ActionsSerializer(serializers.Serializer):
class AccountsGrantedSerializer(serializers.ModelSerializer):
""" 授权的账号序列类 """
# Todo: 添加前端登录逻辑中需要的一些字段,比如:是否需要手动输入密码
# need_manual = serializers.BooleanField(label=_('Need manual input'))
actions = ActionChoicesField(read_only=True)
class Meta:
model = Account
fields = ['id', 'name', 'username', 'actions']
fields = ['id', 'name', 'username', 'secret_type', 'has_secret', 'actions']
read_only_fields = fields

View File

@ -1,8 +1,9 @@
# coding:utf-8
from .asset_permission import asset_permission_urlpatterns
from .user_permission import user_permission_urlpatterns
app_name = 'perms'
urlpatterns = []
urlpatterns += asset_permission_urlpatterns
urlpatterns = asset_permission_urlpatterns \
+ user_permission_urlpatterns

View File

@ -7,81 +7,14 @@ from .. import api
router = BulkRouter()
router.register('asset-permissions', api.AssetPermissionViewSet, 'asset-permission')
router.register('asset-permissions-users-relations', api.AssetPermissionUserRelationViewSet, 'asset-permissions-users-relation')
router.register('asset-permissions-user-groups-relations', api.AssetPermissionUserGroupRelationViewSet, 'asset-permissions-user-groups-relation')
router.register('asset-permissions-assets-relations', api.AssetPermissionAssetRelationViewSet, 'asset-permissions-assets-relation')
router.register('asset-permissions-nodes-relations', api.AssetPermissionNodeRelationViewSet, 'asset-permissions-nodes-relation')
user_permission_urlpatterns = [
# 以 serializer 格式返回
path('<uuid:pk>/assets/', api.UserAllGrantedAssetsApi.as_view(), name='user-assets'),
path('assets/', api.MyAllGrantedAssetsApi.as_view(), name='my-assets'),
# Tree Node 的数据格式返回
path('<uuid:pk>/assets/tree/', api.UserDirectGrantedAssetsAsTreeApi.as_view(), name='user-assets-as-tree'),
path('assets/tree/', api.MyAllAssetsAsTreeApi.as_view(), name='my-assets-as-tree'),
path('ungroup/assets/tree/', api.MyUngroupAssetsAsTreeApi.as_view(), name='my-ungroup-assets-as-tree'),
# 获取用户所有`直接授权的节点`与`直接授权资产`关联的节点
# 以 serializer 格式返回
path('<uuid:pk>/nodes/', api.UserGrantedNodesApi.as_view(), name='user-nodes'),
path('nodes/', api.MyGrantedNodesApi.as_view(), name='my-nodes'),
# 以 Tree Node 的数据格式返回
path('<uuid:pk>/nodes/tree/', api.MyGrantedNodesAsTreeApi.as_view(), name='user-nodes-as-tree'),
path('nodes/tree/', api.MyGrantedNodesAsTreeApi.as_view(), name='my-nodes-as-tree'),
# 一层一层的获取用户授权的节点,
# 以 Serializer 的数据格式返回
path('<uuid:pk>/nodes/children/', api.UserGrantedNodeChildrenForAdminApi.as_view(), name='user-nodes-children'),
path('nodes/children/', api.MyGrantedNodeChildrenApi.as_view(), name='my-nodes-children'),
# 以 Tree Node 的数据格式返回
path('<uuid:pk>/nodes/children/tree/', api.UserGrantedNodeChildrenAsTreeForAdminApi.as_view(), name='user-nodes-children-as-tree'),
# 部分调用位置
# - 普通用户 -> 我的资产 -> 展开节点 时调用
path('nodes/children/tree/', api.MyGrantedNodeChildrenAsTreeApi.as_view(), name='my-nodes-children-as-tree'),
# 此接口会返回整棵树
# 普通用户 -> 命令执行 -> 左侧树
path('nodes-with-assets/tree/', api.MyGrantedNodesWithAssetsAsTreeApi.as_view(), name='my-nodes-with-assets-as-tree'),
# 主要用于 luna 页面,带资产的节点树
path('<uuid:pk>/nodes/children-with-assets/tree/', api.UserGrantedNodeChildrenWithAssetsAsTreeApi.as_view(), name='user-nodes-children-with-assets-as-tree'),
path('nodes/children-with-assets/tree/', api.MyGrantedNodeChildrenWithAssetsAsTreeApi.as_view(), name='my-nodes-children-with-assets-as-tree'),
# 查询授权树上某个节点的所有资产
path('<uuid:pk>/nodes/<uuid:node_id>/assets/', api.UserGrantedNodeAssetsApi.as_view(), name='user-node-assets'),
path('nodes/<uuid:node_id>/assets/', api.MyGrantedNodeAssetsApi.as_view(), name='my-node-assets'),
# 未分组的资产
path('<uuid:pk>/nodes/ungrouped/assets/', api.UserDirectGrantedAssetsApi.as_view(), name='user-ungrouped-assets'),
path('nodes/ungrouped/assets/', api.MyDirectGrantedAssetsApi.as_view(), name='my-ungrouped-assets'),
# 收藏的资产
path('<uuid:pk>/nodes/favorite/assets/', api.UserFavoriteGrantedAssetsApi.as_view(), name='user-ungrouped-assets'),
path('nodes/favorite/assets/', api.MyFavoriteGrantedAssetsApi.as_view(), name='my-ungrouped-assets'),
# 获取授权给用户的所有账号
path('<uuid:pk>/accounts/', api.UserAllGrantedAccountsApi.as_view(), name='user-accounts'),
path('accounts/', api.MyAllGrantedAccountsApi.as_view(), name='my-accounts'),
# 获取授权给用户某个资产的所有账号
path('<uuid:pk>/assets/<uuid:asset_id>/accounts/', api.UserGrantedAssetAccountsApi.as_view(), name='user-asset-accounts'),
path('assets/<uuid:asset_id>/accounts/', api.MyGrantedAssetAccountsApi.as_view(), name='my-asset-accounts'),
# 用户登录资产的特殊账号, @INPUT, @USER 等
path('<uuid:pk>/assets/special-accounts/', api.UserGrantedAssetSpecialAccountsApi.as_view(), name='user-special-accounts'),
path('assets/special-accounts/', api.MyGrantedAssetSpecialAccountsApi.as_view(), name='my-special-accounts'),
]
user_group_permission_urlpatterns = [
# 查询某个用户组授权的资产和资产组
path('<uuid:pk>/assets/', api.UserGroupGrantedAssetsApi.as_view(), name='user-group-assets'),
path('<uuid:pk>/nodes/', api.UserGroupGrantedNodesApi.as_view(), name='user-group-nodes'),
path('<uuid:pk>/nodes/children/', api.UserGroupGrantedNodesApi.as_view(), name='user-group-nodes-children'),
path('<uuid:pk>/nodes/children/tree/', api.UserGroupGrantedNodeChildrenAsTreeApi.as_view(), name='user-group-nodes-children-as-tree'),
path('<uuid:pk>/nodes/<uuid:node_id>/assets/', api.UserGroupGrantedNodeAssetsApi.as_view(), name='user-group-node-assets'),
# 获取所有和资产-用户组关联的账号列表
path('<uuid:pk>/assets/<uuid:asset_id>/accounts/', api.UserGroupGrantedAssetAccountsApi.as_view(), name='user-group-asset-accounts'),
]
router.register('asset-permissions-users-relations', api.AssetPermissionUserRelationViewSet,
'asset-permissions-users-relation')
router.register('asset-permissions-user-groups-relations', api.AssetPermissionUserGroupRelationViewSet,
'asset-permissions-user-groups-relation')
router.register('asset-permissions-assets-relations', api.AssetPermissionAssetRelationViewSet,
'asset-permissions-assets-relation')
router.register('asset-permissions-nodes-relations', api.AssetPermissionNodeRelationViewSet,
'asset-permissions-nodes-relation')
permission_urlpatterns = [
# 授权规则中授权的资产
@ -92,8 +25,6 @@ permission_urlpatterns = [
asset_permission_urlpatterns = [
# Assets
path('users/', include(user_permission_urlpatterns)),
path('user-groups/', include(user_group_permission_urlpatterns)),
path('asset-permissions/', include(permission_urlpatterns)),
]

View File

@ -0,0 +1,80 @@
from django.urls import path, include
from .. import api
user_permission_urlpatterns = [
# 以 serializer 格式返回
path('<uuid:pk>/assets/', api.UserAllGrantedAssetsApi.as_view(), name='user-assets'),
path('assets/', api.MyAllGrantedAssetsApi.as_view(), name='my-assets'),
# Tree Node 的数据格式返回
path('<uuid:pk>/assets/tree/', api.UserDirectGrantedAssetsAsTreeApi.as_view(), name='user-assets-as-tree'),
path('assets/tree/', api.MyAllAssetsAsTreeApi.as_view(), name='my-assets-as-tree'),
path('ungroup/assets/tree/', api.MyUngroupAssetsAsTreeApi.as_view(), name='my-ungroup-assets-as-tree'),
# 获取用户所有`直接授权的节点`与`直接授权资产`关联的节点
# 以 serializer 格式返回
path('<uuid:pk>/nodes/', api.UserGrantedNodesApi.as_view(), name='user-nodes'),
path('nodes/', api.MyGrantedNodesApi.as_view(), name='my-nodes'),
# 以 Tree Node 的数据格式返回
path('<uuid:pk>/nodes/tree/', api.MyGrantedNodesAsTreeApi.as_view(), name='user-nodes-as-tree'),
path('nodes/tree/', api.MyGrantedNodesAsTreeApi.as_view(), name='my-nodes-as-tree'),
# 一层一层的获取用户授权的节点,
# 以 Serializer 的数据格式返回
path('<uuid:pk>/nodes/children/', api.UserGrantedNodeChildrenForAdminApi.as_view(), name='user-nodes-children'),
path('nodes/children/', api.MyGrantedNodeChildrenApi.as_view(), name='my-nodes-children'),
# 以 Tree Node 的数据格式返回
path('<uuid:pk>/nodes/children/tree/', api.UserGrantedNodeChildrenAsTreeForAdminApi.as_view(),
name='user-nodes-children-as-tree'),
# 部分调用位置
# - 普通用户 -> 我的资产 -> 展开节点 时调用
path('nodes/children/tree/', api.MyGrantedNodeChildrenAsTreeApi.as_view(), name='my-nodes-children-as-tree'),
# 此接口会返回整棵树
# 普通用户 -> 命令执行 -> 左侧树
path('nodes-with-assets/tree/', api.MyGrantedNodesWithAssetsAsTreeApi.as_view(),
name='my-nodes-with-assets-as-tree'),
# 主要用于 luna 页面,带资产的节点树
path('<uuid:pk>/nodes/children-with-assets/tree/', api.UserGrantedNodeChildrenWithAssetsAsTreeApi.as_view(),
name='user-nodes-children-with-assets-as-tree'),
path('nodes/children-with-assets/tree/', api.MyGrantedNodeChildrenWithAssetsAsTreeApi.as_view(),
name='my-nodes-children-with-assets-as-tree'),
# 查询授权树上某个节点的所有资产
path('<uuid:pk>/nodes/<uuid:node_id>/assets/', api.UserGrantedNodeAssetsApi.as_view(), name='user-node-assets'),
path('nodes/<uuid:node_id>/assets/', api.MyGrantedNodeAssetsApi.as_view(), name='my-node-assets'),
# 未分组的资产
path('<uuid:pk>/nodes/ungrouped/assets/', api.UserDirectGrantedAssetsApi.as_view(), name='user-ungrouped-assets'),
path('nodes/ungrouped/assets/', api.MyDirectGrantedAssetsApi.as_view(), name='my-ungrouped-assets'),
# 收藏的资产
path('<uuid:pk>/nodes/favorite/assets/', api.UserFavoriteGrantedAssetsApi.as_view(), name='user-ungrouped-assets'),
path('nodes/favorite/assets/', api.MyFavoriteGrantedAssetsApi.as_view(),
name='my-ungrouped-assets'),
# 获取授权给用户某个资产的所有账号
path('<str:user>/assets/<uuid:asset_id>/accounts/', api.UserGrantedAssetAccountsApi.as_view(),
name='user-asset-accounts'),
]
user_group_permission_urlpatterns = [
# 查询某个用户组授权的资产和资产组
path('<uuid:pk>/assets/', api.UserGroupGrantedAssetsApi.as_view(), name='user-group-assets'),
path('<uuid:pk>/nodes/', api.UserGroupGrantedNodesApi.as_view(), name='user-group-nodes'),
path('<uuid:pk>/nodes/children/', api.UserGroupGrantedNodesApi.as_view(), name='user-group-nodes-children'),
path('<uuid:pk>/nodes/children/tree/', api.UserGroupGrantedNodeChildrenAsTreeApi.as_view(),
name='user-group-nodes-children-as-tree'),
path('<uuid:pk>/nodes/<uuid:node_id>/assets/', api.UserGroupGrantedNodeAssetsApi.as_view(),
name='user-group-node-assets'),
# 获取所有和资产-用户组关联的账号列表
path('<uuid:pk>/assets/<uuid:asset_id>/accounts/', api.UserGroupGrantedAssetAccountsApi.as_view(),
name='user-group-asset-accounts'),
]
user_permission_urlpatterns = [
path('users/', include(user_permission_urlpatterns)),
path('user-groups/', include(user_group_permission_urlpatterns)),
]

View File

@ -1,30 +1,31 @@
import time
from collections import defaultdict
from typing import List, Tuple
import time
from django.core.cache import cache
from django.conf import settings
from django.core.cache import cache
from django.db.models import Q, QuerySet
from django.utils.translation import gettext as _
from common.db.models import output_as_string, UnionQuerySet
from common.utils.common import lazyproperty, timeit
from assets.models import (
Asset, FavoriteAsset, AssetQuerySet, NodeQuerySet
)
from assets.utils import NodeAssetsUtil
from common.utils import get_logger
from common.db.models import output_as_string, UnionQuerySet
from common.decorator import on_transaction_commit
from common.utils import get_logger
from common.utils.common import lazyproperty, timeit
from orgs.models import Organization
from orgs.utils import (
tmp_to_org, current_org,
ensure_in_real_or_default_org, tmp_to_root_org
)
from assets.models import (
Asset, FavoriteAsset, AssetQuerySet, NodeQuerySet
)
from users.models import User
from orgs.models import Organization
from perms.locks import UserGrantedTreeRebuildLock
from perms.models import (
AssetPermission, PermNode, UserAssetGrantedTreeNodeRelation
)
from users.models import User
NodeFrom = UserAssetGrantedTreeNodeRelation.NodeFrom
NODE_ONLY_FIELDS = ('id', 'key', 'parent_key', 'org_id')
@ -119,8 +120,7 @@ class UserGrantedTreeRefreshController:
key = cls.key_template.format(user_id=user_id)
p.srem(key, *org_ids)
p.execute()
logger.info(f'Remove orgs from users built tree: users:{user_ids} '
f'orgs:{org_ids}')
logger.info(f'Remove orgs from users built tree: users:{user_ids} orgs:{org_ids}')
@classmethod
def add_need_refresh_orgs_for_users(cls, org_ids, user_ids):
@ -205,28 +205,30 @@ class UserGrantedTreeRefreshController:
user = self.user
with tmp_to_root_org():
UserAssetGrantedTreeNodeRelation.objects.filter(user=user)\
.exclude(org_id__in=self.org_ids)\
UserAssetGrantedTreeNodeRelation.objects.filter(user=user) \
.exclude(org_id__in=self.org_ids) \
.delete()
if force or self.have_need_refresh_orgs():
with UserGrantedTreeRebuildLock(user_id=user.id):
if force:
orgs = self.orgs
self.set_all_orgs_as_built()
else:
orgs = self.get_need_refresh_orgs_and_fill_up()
if not force and not self.have_need_refresh_orgs():
return
for org in orgs:
with tmp_to_org(org):
t_start = time.time()
logger.info(f'Rebuild user tree: user={self.user} org={current_org}')
utils = UserGrantedTreeBuildUtils(user)
utils.rebuild_user_granted_tree()
logger.info(
f'Rebuild user tree ok: cost={time.time() - t_start} '
f'user={self.user} org={current_org}'
)
with UserGrantedTreeRebuildLock(user_id=user.id):
if force:
orgs = self.orgs
self.set_all_orgs_as_built()
else:
orgs = self.get_need_refresh_orgs_and_fill_up()
for org in orgs:
with tmp_to_org(org):
t_start = time.time()
logger.info(f'Rebuild user tree: user={self.user} org={current_org}')
utils = UserGrantedTreeBuildUtils(user)
utils.rebuild_user_granted_tree()
logger.info(
f'Rebuild user tree ok: cost={time.time() - t_start} '
f'user={self.user} org={current_org}'
)
class UserGrantedUtilsBase:
@ -427,8 +429,8 @@ class UserGrantedTreeBuildUtils(UserGrantedUtilsBase):
for node_id, asset_id in node_asset_pairs:
if node_id not in node_id_key_mapper:
continue
nkey = node_id_key_mapper[node_id]
nodekey_assetsid_mapper[nkey].add(asset_id)
node_key = node_id_key_mapper[node_id]
nodekey_assetsid_mapper[node_key].add(asset_id)
util = NodeAssetsUtil(nodes, nodekey_assetsid_mapper)
util.generate()
@ -604,7 +606,10 @@ class UserGrantedNodesQueryUtils(UserGrantedUtilsBase):
def get_top_level_nodes(self):
nodes = self.get_special_nodes()
real_nodes = self.get_indirect_granted_node_children('')
nodes.extend(self.sort(real_nodes))
nodes.extend(real_nodes)
if len(real_nodes) == 1:
children = self.get_node_children(real_nodes[0].key)
nodes.extend(children)
return nodes
def get_ungrouped_node(self):
@ -649,11 +654,9 @@ class UserGrantedNodesQueryUtils(UserGrantedUtilsBase):
:param with_special:
:return:
"""
nodes = PermNode.objects.filter(
granted_node_rels__user=self.user
).annotate(
**PermNode.annotate_granted_node_rel_fields
).distinct()
nodes = PermNode.objects.filter(granted_node_rels__user=self.user) \
.annotate(**PermNode.annotate_granted_node_rel_fields) \
.distinct()
key_to_node_mapper = {}
nodes_descendant_q = Q()

View File

@ -2,16 +2,15 @@ from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from common.permissions import IsServiceAccount
from common.drf.api import JMSModelViewSet
from common.permissions import IsServiceAccount
from orgs.utils import tmp_to_builtin_org
from terminal.models import AppletHost, AppletHostDeployment
from terminal.serializers import (
AppletHostSerializer, AppletHostDeploymentSerializer,
AppletHostStartupSerializer
AppletHostStartupSerializer, AppletHostDeployAppletSerializer
)
from terminal.models import AppletHost, AppletHostDeployment
from terminal.tasks import run_applet_host_deployment
from terminal.tasks import run_applet_host_deployment, run_applet_host_deployment_install_applet
__all__ = ['AppletHostViewSet', 'AppletHostDeploymentViewSet']
@ -41,11 +40,24 @@ class AppletHostViewSet(JMSModelViewSet):
class AppletHostDeploymentViewSet(viewsets.ModelViewSet):
serializer_class = AppletHostDeploymentSerializer
queryset = AppletHostDeployment.objects.all()
rbac_perms = (
('applets', 'terminal.view_AppletHostDeployment'),
)
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
instance = serializer.save()
task = run_applet_host_deployment.delay(instance.id)
instance.save_task(task.id)
return Response({'task': str(task.id)}, status=201)
@action(methods=['post'], detail=False, serializer_class=AppletHostDeployAppletSerializer)
def applets(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
applet_id = serializer.validated_data.get('applet_id')
instance = serializer.save()
task = run_applet_host_deployment_install_applet.delay(instance.id, applet_id)
instance.save_task(task.id)
return Response({'task': str(task.id)}, status=201)

View File

@ -1,22 +1,23 @@
import os
import datetime
import shutil
import os
import yaml
from django.utils import timezone
from django.conf import settings
from django.utils import timezone
from common.utils import get_logger
from common.db.utils import safe_db_connection
from common.utils import get_logger
from ops.ansible import PlaybookRunner, JMSInventory
from terminal.models import Applet, AppletHostDeployment
logger = get_logger(__name__)
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
class DeployAppletHostManager:
def __init__(self, deployment):
def __init__(self, deployment: AppletHostDeployment, applet: Applet = None):
self.deployment = deployment
self.applet = applet
self.run_dir = self.get_run_dir()
@staticmethod
@ -25,29 +26,56 @@ class DeployAppletHostManager:
now = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
return os.path.join(base, now)
def generate_playbook(self):
playbook_src = os.path.join(CURRENT_DIR, "playbook.yml")
base_site_url = settings.BASE_SITE_URL
def run(self, **kwargs):
self._run(self._run_initial_deploy, **kwargs)
def install_applet(self, **kwargs):
self._run(self._run_install_applet, **kwargs)
def _run_initial_deploy(self, **kwargs):
playbook = self.generate_initial_playbook
return self._run_playbook(playbook, **kwargs)
def _run_install_applet(self, **kwargs):
if self.applet:
generate_playbook = self.generate_install_applet_playbook
else:
generate_playbook = self.generate_install_all_playbook
return self._run_playbook(generate_playbook, **kwargs)
def generate_initial_playbook(self):
site_url = settings.SITE_URL
bootstrap_token = settings.BOOTSTRAP_TOKEN
host_id = str(self.deployment.host.id)
if not base_site_url:
base_site_url = "http://localhost:8080"
with open(playbook_src) as f:
plays = yaml.safe_load(f)
for play in plays:
play["vars"].update(self.deployment.host.deploy_options)
play["vars"]["DownloadHost"] = base_site_url + "/download"
play["vars"]["CORE_HOST"] = base_site_url
play["vars"]["BOOTSTRAP_TOKEN"] = bootstrap_token
play["vars"]["HOST_ID"] = host_id
play["vars"]["HOST_NAME"] = self.deployment.host.name
if not site_url:
site_url = "http://localhost:8080"
options = self.deployment.host.deploy_options
playbook_dir = os.path.join(self.run_dir, "playbook")
playbook_dst = os.path.join(playbook_dir, "main.yml")
os.makedirs(playbook_dir, exist_ok=True)
with open(playbook_dst, "w") as f:
yaml.safe_dump(plays, f)
return playbook_dst
def handler(plays):
for play in plays:
play["vars"].update(options)
play["vars"]["DownloadHost"] = site_url + "/download"
play["vars"]["CORE_HOST"] = site_url
play["vars"]["BOOTSTRAP_TOKEN"] = bootstrap_token
play["vars"]["HOST_ID"] = host_id
play["vars"]["HOST_NAME"] = self.deployment.host.name
return self._generate_playbook("playbook.yml", handler)
def generate_install_all_playbook(self):
return self._generate_playbook("install_all.yml")
def generate_install_applet_playbook(self):
applet_name = self.applet.name
options = self.deployment.host.deploy_options
def handler(plays):
for play in plays:
play["vars"].update(options)
play["vars"]["applet_name"] = applet_name
return plays
return self._generate_playbook("install_applet.yml", handler)
def generate_inventory(self):
inventory = JMSInventory(
@ -58,18 +86,31 @@ class DeployAppletHostManager:
inventory.write_to_file(inventory_path)
return inventory_path
def _run(self, **kwargs):
def _generate_playbook(self, playbook_template_name, plays_handler: callable = None):
playbook_src = os.path.join(CURRENT_DIR, playbook_template_name)
with open(playbook_src) as f:
plays = yaml.safe_load(f)
if plays_handler:
plays = plays_handler(plays)
playbook_dir = os.path.join(self.run_dir, "playbook")
playbook_dst = os.path.join(playbook_dir, "main.yml")
os.makedirs(playbook_dir, exist_ok=True)
with open(playbook_dst, "w") as f:
yaml.safe_dump(plays, f)
return playbook_dst
def _run_playbook(self, generate_playbook: callable, **kwargs):
inventory = self.generate_inventory()
playbook = self.generate_playbook()
playbook = generate_playbook()
runner = PlaybookRunner(
inventory=inventory, playbook=playbook, project_dir=self.run_dir
)
return runner.run(**kwargs)
def run(self, **kwargs):
def _run(self, cb_func: callable, **kwargs):
try:
self.deployment.date_start = timezone.now()
cb = self._run(**kwargs)
cb = cb_func(**kwargs)
self.deployment.status = cb.status
except Exception as e:
logger.error("Error: {}".format(e))

View File

@ -0,0 +1,8 @@
---
- hosts: all
tasks:
- name: Install all applets
ansible.windows.win_shell:
"tinkerd install all"

View File

@ -0,0 +1,11 @@
---
- hosts: all
vars:
applet_name: chrome
tasks:
- name: Install applet
ansible.windows.win_shell:
"tinkerd install --name {{ applet_name }}"
when: applet_name != 'all'

View File

@ -3,7 +3,6 @@
- hosts: all
vars:
DownloadHost: https://demo.jumpserver.org/download
Initial: 0
HOST_NAME: test
HOST_ID: 00000000-0000-0000-0000-000000000000
CORE_HOST: https://demo.jumpserver.org
@ -17,166 +16,166 @@
TinkerInstaller: Tinker_Installer_v0.0.1.exe
tasks:
- name: Install RDS-Licensing (RDS)
ansible.windows.win_feature:
name: RDS-Licensing
state: present
include_management_tools: yes
when: RDS_Licensing
- name: Install RDS-Licensing (RDS)
ansible.windows.win_feature:
name: RDS-Licensing
state: present
include_management_tools: yes
when: RDS_Licensing
- name: Install RDS-RD-Server (RDS)
ansible.windows.win_feature:
name: RDS-RD-Server
state: present
include_management_tools: yes
register: rds_install
- name: Install RDS-RD-Server (RDS)
ansible.windows.win_feature:
name: RDS-RD-Server
state: present
include_management_tools: yes
register: rds_install
- name: Download JumpServer Tinker installer (jumpserver)
ansible.windows.win_get_url:
- name: Download JumpServer Tinker installer (jumpserver)
ansible.windows.win_get_url:
url: "{{ DownloadHost }}/{{ TinkerInstaller }}"
dest: "{{ ansible_env.TEMP }}\\{{ TinkerInstaller }}"
- name: Install JumpServer Tinker (jumpserver)
ansible.windows.win_package:
path: "{{ ansible_env.TEMP }}\\{{ TinkerInstaller }}"
arguments:
- /VERYSILENT
- /SUPPRESSMSGBOXES
- /NORESTART
state: present
- name: Install JumpServer Tinker (jumpserver)
ansible.windows.win_package:
path: "{{ ansible_env.TEMP }}\\{{ TinkerInstaller }}"
arguments:
- /VERYSILENT
- /SUPPRESSMSGBOXES
- /NORESTART
state: present
- name: Set remote-server on the global system path (remote-server)
ansible.windows.win_path:
elements:
- '%USERPROFILE%\AppData\Local\Programs\Tinker\'
scope: user
- name: Set remote-server on the global system path (remote-server)
ansible.windows.win_path:
elements:
- '%USERPROFILE%\AppData\Local\Programs\Tinker\'
scope: user
- name: Download python-3.10.8
ansible.windows.win_get_url:
url: "{{ DownloadHost }}/python-3.10.8-amd64.exe"
dest: "{{ ansible_env.TEMP }}\\python-3.10.8-amd64.exe"
- name: Download python-3.10.8
ansible.windows.win_get_url:
url: "{{ DownloadHost }}/python-3.10.8-amd64.exe"
dest: "{{ ansible_env.TEMP }}\\python-3.10.8-amd64.exe"
- name: Install the python-3.10.8
ansible.windows.win_package:
path: "{{ ansible_env.TEMP }}\\python-3.10.8-amd64.exe"
product_id: '{371d0d73-d418-4ffe-b280-58c3e7987525}'
arguments:
- /quiet
- InstallAllUsers=1
- PrependPath=1
- Include_test=0
- Include_launcher=0
state: present
register: win_install_python
- name: Install the python-3.10.8
ansible.windows.win_package:
path: "{{ ansible_env.TEMP }}\\python-3.10.8-amd64.exe"
product_id: '{371d0d73-d418-4ffe-b280-58c3e7987525}'
arguments:
- /quiet
- InstallAllUsers=1
- PrependPath=1
- Include_test=0
- Include_launcher=0
state: present
register: win_install_python
- name: Reboot if installing requires it
ansible.windows.win_reboot:
post_reboot_delay: 10
test_command: whoami
when: rds_install.reboot_required or win_install_python.reboot_required
- name: Reboot if installing requires it
ansible.windows.win_reboot:
post_reboot_delay: 10
test_command: whoami
when: rds_install.reboot_required or win_install_python.reboot_required
- name: Set RDS LicenseServer (regedit)
ansible.windows.win_regedit:
path: HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services
name: LicenseServers
data: "{{ RDS_LicenseServer }}"
type: string
- name: Set RDS LicenseServer (regedit)
ansible.windows.win_regedit:
path: HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services
name: LicenseServers
data: "{{ RDS_LicenseServer }}"
type: string
- name: Set RDS LicensingMode (regedit)
ansible.windows.win_regedit:
path: HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services
name: LicensingMode
data: "{{ RDS_LicensingMode }}"
type: dword
- name: Set RDS LicensingMode (regedit)
ansible.windows.win_regedit:
path: HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services
name: LicensingMode
data: "{{ RDS_LicensingMode }}"
type: dword
- name: Set RDS fSingleSessionPerUser (regedit)
ansible.windows.win_regedit:
path: HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services
name: fSingleSessionPerUser
data: "{{ RDS_fSingleSessionPerUser }}"
type: dword
- name: Set RDS fSingleSessionPerUser (regedit)
ansible.windows.win_regedit:
path: HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services
name: fSingleSessionPerUser
data: "{{ RDS_fSingleSessionPerUser }}"
type: dword
- name: Set RDS MaxDisconnectionTime (regedit)
ansible.windows.win_regedit:
path: HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services
name: MaxDisconnectionTime
data: "{{ RDS_MaxDisconnectionTime }}"
type: dword
when: RDS_MaxDisconnectionTime >= 60000
- name: Set RDS MaxDisconnectionTime (regedit)
ansible.windows.win_regedit:
path: HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services
name: MaxDisconnectionTime
data: "{{ RDS_MaxDisconnectionTime }}"
type: dword
when: RDS_MaxDisconnectionTime >= 60000
- name: Set RDS RemoteAppLogoffTimeLimit (regedit)
ansible.windows.win_regedit:
path: HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services
name: RemoteAppLogoffTimeLimit
data: "{{ RDS_RemoteAppLogoffTimeLimit }}"
type: dword
- name: Set RDS RemoteAppLogoffTimeLimit (regedit)
ansible.windows.win_regedit:
path: HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services
name: RemoteAppLogoffTimeLimit
data: "{{ RDS_RemoteAppLogoffTimeLimit }}"
type: dword
- name: Download pip packages
ansible.windows.win_get_url:
url: "{{ DownloadHost }}/pip_packages_v0.0.1.zip"
dest: "{{ ansible_env.TEMP }}\\pip_packages_v0.0.1.zip"
- name: Download pip packages
ansible.windows.win_get_url:
url: "{{ DownloadHost }}/pip_packages_v0.0.1.zip"
dest: "{{ ansible_env.TEMP }}\\pip_packages_v0.0.1.zip"
- name: Unzip pip_packages
community.windows.win_unzip:
src: "{{ ansible_env.TEMP }}\\pip_packages_v0.0.1.zip"
dest: "{{ ansible_env.TEMP }}"
- name: Unzip pip_packages
community.windows.win_unzip:
src: "{{ ansible_env.TEMP }}\\pip_packages_v0.0.1.zip"
dest: "{{ ansible_env.TEMP }}"
- name: Install python requirements offline
ansible.windows.win_shell: >
- name: Install python requirements offline
ansible.windows.win_shell: >
pip install -r '{{ ansible_env.TEMP }}\pip_packages_v0.0.1\requirements.txt'
--no-index --find-links='{{ ansible_env.TEMP }}\pip_packages_v0.0.1'
- name: Download chromedriver (chrome)
ansible.windows.win_get_url:
url: "{{ DownloadHost }}/chromedriver_win32.107.zip"
dest: "{{ ansible_env.TEMP }}\\chromedriver_win32.107.zip"
- name: Download chromedriver (chrome)
ansible.windows.win_get_url:
url: "{{ DownloadHost }}/chromedriver_win32.107.zip"
dest: "{{ ansible_env.TEMP }}\\chromedriver_win32.107.zip"
- name: Unzip chromedriver (chrome)
community.windows.win_unzip:
src: "{{ ansible_env.TEMP }}\\chromedriver_win32.107.zip"
dest: C:\Program Files\JumpServer\drivers
- name: Unzip chromedriver (chrome)
community.windows.win_unzip:
src: "{{ ansible_env.TEMP }}\\chromedriver_win32.107.zip"
dest: C:\Program Files\JumpServer\drivers
- name: Set chromedriver on the global system path (chrome)
ansible.windows.win_path:
elements:
- 'C:\Program Files\JumpServer\drivers'
- name: Set chromedriver on the global system path (chrome)
ansible.windows.win_path:
elements:
- 'C:\Program Files\JumpServer\drivers'
- name: Download chrome msi package (chrome)
ansible.windows.win_get_url:
url: "{{ DownloadHost }}/googlechromestandaloneenterprise64.msi"
dest: "{{ ansible_env.TEMP }}\\googlechromestandaloneenterprise64.msi"
- name: Download chrome msi package (chrome)
ansible.windows.win_get_url:
url: "{{ DownloadHost }}/googlechromestandaloneenterprise64.msi"
dest: "{{ ansible_env.TEMP }}\\googlechromestandaloneenterprise64.msi"
- name: Install chrome (chrome)
ansible.windows.win_package:
path: "{{ ansible_env.TEMP }}\\googlechromestandaloneenterprise64.msi"
state: present
arguments:
- /quiet
- name: Install chrome (chrome)
ansible.windows.win_package:
path: "{{ ansible_env.TEMP }}\\googlechromestandaloneenterprise64.msi"
state: present
arguments:
- /quiet
- name: Generate tinkerd component config
ansible.windows.win_shell:
"tinkerd config --hostname {{ HOST_NAME }} --core_host {{ CORE_HOST }}
- name: Generate tinkerd component config
ansible.windows.win_shell:
"tinkerd config --hostname {{ HOST_NAME }} --core_host {{ CORE_HOST }}
--token {{ BOOTSTRAP_TOKEN }} --host_id {{ HOST_ID }}"
- name: Install tinkerd service
ansible.windows.win_shell:
"tinkerd service install"
- name: Install tinkerd service
ansible.windows.win_shell:
"tinkerd service install"
- name: Start tinkerd service
ansible.windows.win_shell:
"tinkerd service start"
- name: Start tinkerd service
ansible.windows.win_shell:
"tinkerd service start"
- name: Wait Tinker api health
ansible.windows.win_uri:
url: http://localhost:6068/api/health/
status_code: 200
method: GET
register: _result
until: _result.status_code == 200
retries: 30
delay: 5
- name: Wait Tinker api health
ansible.windows.win_uri:
url: http://localhost:6068/api/health/
status_code: 200
method: GET
register: _result
until: _result.status_code == 200
retries: 30
delay: 5
- name: Sync all remote applets
ansible.windows.win_shell:
"tinkerd install all"
- name: Sync all remote applets
ansible.windows.win_shell:
"tinkerd install all"

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.14 on 2022-11-15 05:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('terminal', '0058_auto_20221103_1624'),
]
operations = [
migrations.AddField(
model_name='applethostdeployment',
name='task',
field=models.UUIDField(null=True, verbose_name='Task'),
),
]

View File

@ -105,8 +105,23 @@ class AppletHostDeployment(JMSBaseModel):
date_start = models.DateTimeField(null=True, verbose_name=_('Date start'), db_index=True)
date_finished = models.DateTimeField(null=True, verbose_name=_("Date finished"))
comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
task = models.UUIDField(null=True, verbose_name=_('Task'))
def start(self, **kwargs):
from ...automations.deploy_applet_host import DeployAppletHostManager
manager = DeployAppletHostManager(self)
manager.run(**kwargs)
def install_applet(self, applet_id, **kwargs):
from ...automations.deploy_applet_host import DeployAppletHostManager
from .applet import Applet
if applet_id:
applet = Applet.objects.get(id=applet_id)
else:
applet = None
manager = DeployAppletHostManager(self, applet=applet)
manager.install_applet(**kwargs)
def save_task(self, task):
self.task = task
self.save(update_fields=['task'])

View File

@ -1,19 +1,18 @@
from rest_framework import serializers
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from common.validators import ProjectUniqueValidator
from common.drf.fields import ObjectRelatedField, LabeledChoiceField
from assets.models import Platform, Account
from assets.serializers import HostSerializer
from ..models import AppletHost, AppletHostDeployment, Applet
from common.drf.fields import LabeledChoiceField
from common.validators import ProjectUniqueValidator
from .applet import AppletSerializer
from .. import const
from ..models import AppletHost, AppletHostDeployment
__all__ = [
'AppletHostSerializer', 'AppletHostDeploymentSerializer',
'AppletHostAccountSerializer', 'AppletHostAppletReportSerializer',
'AppletHostStartupSerializer',
'AppletHostStartupSerializer', 'AppletHostDeployAppletSerializer'
]
@ -29,7 +28,8 @@ class DeployOptionsSerializer(serializers.Serializer):
RDS_Licensing = serializers.BooleanField(default=False, label=_("RDS Licensing"))
RDS_LicenseServer = serializers.CharField(default='127.0.0.1', label=_('RDS License Server'), max_length=1024)
RDS_LicensingMode = serializers.ChoiceField(choices=LICENSE_MODE_CHOICES, default=4, label=_('RDS Licensing Mode'))
RDS_fSingleSessionPerUser = serializers.ChoiceField(choices=SESSION_PER_USER, default=1, label=_("RDS fSingleSessionPerUser"))
RDS_fSingleSessionPerUser = serializers.ChoiceField(choices=SESSION_PER_USER, default=1,
label=_("RDS fSingleSessionPerUser"))
RDS_MaxDisconnectionTime = serializers.IntegerField(default=60000, label=_("RDS Max Disconnection Time"))
RDS_RemoteAppLogoffTimeLimit = serializers.IntegerField(default=0, label=_("RDS Remote App Logoff Time Limit"))
@ -86,7 +86,7 @@ class HostAppletSerializer(AppletSerializer):
class AppletHostDeploymentSerializer(serializers.ModelSerializer):
class Meta:
model = AppletHostDeployment
fields_mini = ['id', 'host', 'status']
fields_mini = ['id', 'host', 'status', 'task']
read_only_fields = [
'status', 'date_created', 'date_updated',
'date_start', 'date_finished'
@ -94,6 +94,18 @@ class AppletHostDeploymentSerializer(serializers.ModelSerializer):
fields = fields_mini + ['comment'] + read_only_fields
class AppletHostDeployAppletSerializer(AppletHostDeploymentSerializer):
applet_id = serializers.UUIDField(write_only=True, allow_null=True, required=False)
class Meta(AppletHostDeploymentSerializer.Meta):
fields = AppletHostDeploymentSerializer.Meta.fields + ['applet_id']
def create(self, validated_data):
applet_id = validated_data.pop('applet_id', None)
deployment = super().create(validated_data)
return deployment
class AppletHostAccountSerializer(serializers.ModelSerializer):
class Meta:
model = Account

View File

@ -1,26 +1,25 @@
# -*- coding: utf-8 -*-
#
import datetime
import os
import subprocess
import datetime
from celery import shared_task
from celery.utils.log import get_task_logger
from django.utils import timezone
from django.core.files.storage import default_storage
from django.utils import timezone
from common.utils import get_log_keep_day
from ops.celery.decorator import (
register_as_period_task, after_app_ready_start,
after_app_shutdown_clean_periodic
)
from .models import (
Status, Session, Command, Task, AppletHost,
AppletHostDeployment
)
from orgs.utils import tmp_to_builtin_org
from .backends import server_replay_storage
from .models import (
Status, Session, Command, Task, AppletHostDeployment
)
from .utils import find_session_replay_local
CACHE_REFRESH_INTERVAL = 10
@ -57,7 +56,7 @@ def clean_orphan_session():
@shared_task
@register_as_period_task(interval=3600*24)
@register_as_period_task(interval=3600 * 24)
@after_app_ready_start
@after_app_shutdown_clean_periodic
def clean_expired_session_period():
@ -114,3 +113,10 @@ def run_applet_host_deployment(did):
with tmp_to_builtin_org(system=1):
deployment = AppletHostDeployment.objects.get(id=did)
deployment.start()
@shared_task
def run_applet_host_deployment_install_applet(did, applet_id):
with tmp_to_builtin_org(system=1):
deployment = AppletHostDeployment.objects.get(id=did)
deployment.install_applet(applet_id)

View File

@ -2,22 +2,20 @@
#
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.exceptions import MethodNotAllowed
from rest_framework.response import Response
from common.const.http import POST, PUT, PATCH
from common.mixins.api import CommonApiMixin
from orgs.utils import tmp_to_root_org
from rbac.permissions import RBACPermission
from tickets import serializers
from tickets import filters
from tickets.permissions.ticket import IsAssignee, IsApplicant
from tickets import serializers
from tickets.models import (
Ticket, ApplyAssetTicket, ApplyLoginTicket,
ApplyLoginAssetTicket, ApplyCommandTicket
)
from tickets.permissions.ticket import IsAssignee, IsApplicant
__all__ = [
'TicketViewSet', 'ApplyAssetTicketViewSet',
@ -27,10 +25,8 @@ __all__ = [
class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet):
serializer_class = serializers.TicketDisplaySerializer
serializer_class = serializers.TicketSerializer
serializer_classes = {
'list': serializers.TicketListSerializer,
'open': serializers.TicketApplySerializer,
'approve': serializers.TicketApproveSerializer
}
model = Ticket
@ -40,8 +36,8 @@ class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet):
'title', 'type', 'status'
]
ordering_fields = (
'title', 'status', 'state',
'action_display', 'date_created', 'serial_num',
'title', 'status', 'state', 'action_display',
'date_created', 'serial_num',
)
ordering = ('-date_created',)
rbac_perms = {
@ -98,11 +94,7 @@ class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet):
class ApplyAssetTicketViewSet(TicketViewSet):
serializer_class = serializers.ApplyAssetDisplaySerializer
serializer_classes = {
'open': serializers.ApplyAssetSerializer,
'approve': serializers.ApproveAssetSerializer
}
serializer_class = serializers.ApplyAssetSerializer
model = ApplyAssetTicket
filterset_class = filters.ApplyAssetTicketFilter

View File

@ -1,6 +1,7 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from perms.const import ActionChoices
from .general import Ticket
__all__ = ['ApplyAssetTicket']
@ -14,13 +15,6 @@ class ApplyAssetTicket(Ticket):
# 申请信息
apply_assets = models.ManyToManyField('assets.Asset', verbose_name=_('Apply assets'))
apply_accounts = models.JSONField(default=list, verbose_name=_('Apply accounts'))
apply_actions = models.IntegerField(default=1, verbose_name=_('Actions'))
apply_actions = models.IntegerField(verbose_name=_('Actions'), default=ActionChoices.all())
apply_date_start = models.DateTimeField(verbose_name=_('Date start'), null=True)
apply_date_expired = models.DateTimeField(verbose_name=_('Date expired'), null=True)
@property
def apply_actions_display(self):
return 'Todo'
def get_apply_actions_display(self):
return ', '.join(self.apply_actions_display)

View File

@ -1,40 +1,40 @@
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from perms.serializers.permission import ActionChoicesField
from perms.models import AssetPermission
from orgs.utils import tmp_to_org
from assets.models import Asset, Node
from common.drf.fields import ObjectRelatedField
from perms.models import AssetPermission
from perms.serializers.permission import ActionChoicesField
from tickets.models import ApplyAssetTicket
from .common import BaseApplyAssetSerializer
from .ticket import TicketApplySerializer
from .common import BaseApplyAssetApplicationSerializer
__all__ = ['ApplyAssetSerializer', 'ApplyAssetDisplaySerializer', 'ApproveAssetSerializer']
__all__ = ['ApplyAssetSerializer']
asset_or_node_help_text = _("Select at least one asset or node")
class ApplyAssetSerializer(BaseApplyAssetApplicationSerializer, TicketApplySerializer):
apply_actions = ActionChoicesField(required=True, allow_empty=False)
class ApplyAssetSerializer(BaseApplyAssetSerializer, TicketApplySerializer):
apply_assets = ObjectRelatedField(queryset=Asset.objects, many=True, required=False, label=_('Apply assets'))
apply_nodes = ObjectRelatedField(queryset=Node.objects, many=True, required=False, label=_('Apply nodes'))
apply_actions = ActionChoicesField(required=False, allow_null=True, label=_("Apply actions"))
permission_model = AssetPermission
class Meta:
class Meta(TicketApplySerializer.Meta):
model = ApplyAssetTicket
fields_mini = ['id', 'title']
writeable_fields = [
'id', 'title', 'type', 'apply_nodes', 'apply_assets',
'id', 'title', 'apply_nodes', 'apply_assets',
'apply_accounts', 'apply_actions', 'org_id', 'comment',
'apply_date_start', 'apply_date_expired'
]
fields = TicketApplySerializer.Meta.fields + writeable_fields + [
'apply_permission_name', 'apply_actions_display'
]
fields = TicketApplySerializer.Meta.fields + writeable_fields + ['apply_permission_name', ]
read_only_fields = list(set(fields) - set(writeable_fields))
ticket_extra_kwargs = TicketApplySerializer.Meta.extra_kwargs
extra_kwargs = {
'apply_nodes': {'required': False, 'allow_empty': True},
'apply_assets': {'required': False, 'allow_empty': True},
'apply_accounts': {'required': False, 'allow_empty': True},
'apply_nodes': {'required': False},
'apply_assets': {'required': False},
'apply_accounts': {'required': False},
}
extra_kwargs.update(ticket_extra_kwargs)
@ -45,9 +45,11 @@ class ApplyAssetSerializer(BaseApplyAssetApplicationSerializer, TicketApplySeria
return self.filter_many_to_many_field(Asset, assets)
def validate(self, attrs):
attrs['type'] = 'apply_asset'
attrs = super().validate(attrs)
if self.is_final_approval and (
not attrs.get('apply_nodes') and not attrs.get('apply_assets')
not attrs.get('apply_nodes')
and not attrs.get('apply_assets')
):
raise serializers.ValidationError({
'apply_nodes': asset_or_node_help_text,
@ -56,29 +58,7 @@ class ApplyAssetSerializer(BaseApplyAssetApplicationSerializer, TicketApplySeria
return attrs
class ApproveAssetSerializer(ApplyAssetSerializer):
class Meta(ApplyAssetSerializer.Meta):
read_only_fields = ApplyAssetSerializer.Meta.read_only_fields + [
'title', 'type'
]
class ApplyAssetDisplaySerializer(ApplyAssetSerializer):
apply_nodes = serializers.SerializerMethodField()
apply_assets = serializers.SerializerMethodField()
class Meta:
model = ApplyAssetSerializer.Meta.model
fields = ApplyAssetSerializer.Meta.fields
read_only_fields = fields
@staticmethod
def get_apply_nodes(instance):
with tmp_to_org(instance.org_id):
return instance.apply_nodes.values_list('id', flat=True)
@staticmethod
def get_apply_assets(instance):
with tmp_to_org(instance.org_id):
return instance.apply_assets.values_list('id', flat=True)
@classmethod
def setup_eager_loading(cls, queryset):
queryset = queryset.prefetch_related('apply_nodes', 'apply_assets')
return queryset

View File

@ -1,12 +1,12 @@
from django.db.transaction import atomic
from django.db.models import Model
from django.db.transaction import atomic
from django.utils.translation import ugettext as _
from rest_framework import serializers
from orgs.utils import tmp_to_org
from tickets.models import Ticket
__all__ = ['DefaultPermissionName', 'get_default_permission_name', 'BaseApplyAssetApplicationSerializer']
__all__ = ['DefaultPermissionName', 'get_default_permission_name', 'BaseApplyAssetSerializer']
def get_default_permission_name(ticket):
@ -34,7 +34,7 @@ class DefaultPermissionName(object):
return self.default
class BaseApplyAssetApplicationSerializer(serializers.Serializer):
class BaseApplyAssetSerializer(serializers.Serializer):
permission_model: Model
@property

View File

@ -3,31 +3,35 @@
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from orgs.models import Organization
from common.drf.fields import LabeledChoiceField
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
from orgs.models import Organization
from tickets.const import TicketType, TicketStatus, TicketState
from tickets.models import Ticket, TicketFlow
from tickets.const import TicketType
__all__ = [
'TicketDisplaySerializer', 'TicketApplySerializer', 'TicketListSerializer', 'TicketApproveSerializer'
'TicketApplySerializer', 'TicketApproveSerializer', 'TicketSerializer',
]
class TicketSerializer(OrgResourceModelSerializerMixin):
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display'))
status_display = serializers.ReadOnlyField(source='get_status_display', label=_('Status display'))
type = LabeledChoiceField(choices=TicketType.choices, read_only=True, label=_('Type'))
status = LabeledChoiceField(choices=TicketStatus.choices, read_only=True, label=_('Status'))
state = LabeledChoiceField(choices=TicketState.choices, read_only=True, label=_("State"))
class Meta:
model = Ticket
fields_mini = ['id', 'title']
fields_small = fields_mini + [
'type', 'type_display', 'status', 'status_display',
'state', 'approval_step', 'rel_snapshot', 'comment',
'type', 'status', 'state', 'approval_step', 'comment',
'date_created', 'date_updated', 'org_id', 'rel_snapshot',
'process_map', 'org_name', 'serial_num'
]
fields_fk = ['applicant', ]
fields = fields_small + fields_fk
extra_kwargs = {
'type': {'required': True}
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -41,43 +45,20 @@ class TicketSerializer(OrgResourceModelSerializerMixin):
choices.pop(TicketType.general, None)
tp._choices = choices
class TicketListSerializer(TicketSerializer):
class Meta:
model = Ticket
fields = [
'id', 'title', 'serial_num', 'type', 'type_display', 'status',
'state', 'rel_snapshot', 'date_created', 'rel_snapshot'
]
read_only_fields = fields
class TicketDisplaySerializer(TicketSerializer):
class Meta:
model = Ticket
fields = TicketSerializer.Meta.fields
read_only_fields = fields
@classmethod
def setup_eager_loading(cls, queryset):
queryset = queryset.prefetch_related('ticket_steps')
return queryset
class TicketApproveSerializer(TicketSerializer):
class Meta:
model = Ticket
class Meta(TicketSerializer.Meta):
fields = TicketSerializer.Meta.fields
read_only_fields = fields
class TicketApplySerializer(TicketSerializer):
org_id = serializers.CharField(
required=True, max_length=36,
allow_blank=True, label=_("Organization")
)
class Meta:
model = Ticket
fields = TicketSerializer.Meta.fields
extra_kwargs = {
'type': {'required': True}
}
org_id = serializers.CharField(required=True, max_length=36, allow_blank=True, label=_("Organization"))
@staticmethod
def validate_org_id(org_id):
@ -91,10 +72,13 @@ class TicketApplySerializer(TicketSerializer):
if self.instance:
return attrs
print("Attrs: ", attrs)
ticket_type = attrs.get('type')
org_id = attrs.get('org_id')
flow = TicketFlow.get_org_related_flows(org_id=org_id)\
flow = TicketFlow.get_org_related_flows(org_id=org_id) \
.filter(type=ticket_type).first()
if flow:
attrs['flow'] = flow
else: