mirror of https://github.com/jumpserver/jumpserver
commit
21c41a6334
|
@ -0,0 +1,18 @@
|
|||
name: Send LGTM reaction
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@1.0.0
|
||||
- uses: micnncim/action-lgtm-reaction@master
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
trigger: '["^.?lgtm$"]'
|
|
@ -1,4 +1,3 @@
|
|||
from .application import *
|
||||
from .account import *
|
||||
from .mixin import *
|
||||
from .remote_app import *
|
||||
|
|
|
@ -41,7 +41,7 @@ class AppType(models.TextChoices):
|
|||
def category_types_mapper(cls):
|
||||
return {
|
||||
AppCategory.db: [
|
||||
cls.mysql, cls.oracle, cls.pgsql, cls.mariadb,
|
||||
cls.mysql, cls.mariadb, cls.oracle, cls.pgsql,
|
||||
cls.sqlserver, cls.redis, cls.mongodb
|
||||
],
|
||||
AppCategory.remote_app: [
|
||||
|
|
|
@ -8,6 +8,7 @@ from django.conf import settings
|
|||
from orgs.mixins.models import OrgModelMixin
|
||||
from common.mixins import CommonModelMixin
|
||||
from common.tree import TreeNode
|
||||
from common.utils import is_uuid
|
||||
from assets.models import Asset, SystemUser
|
||||
|
||||
from ..utils import KubernetesTree
|
||||
|
@ -19,6 +20,7 @@ class ApplicationTreeNodeMixin:
|
|||
name: str
|
||||
type: str
|
||||
category: str
|
||||
attrs: dict
|
||||
|
||||
@staticmethod
|
||||
def create_tree_id(pid, type, v):
|
||||
|
@ -99,6 +101,7 @@ class ApplicationTreeNodeMixin:
|
|||
temp_pid = pid
|
||||
type_category_mapper = const.AppType.type_category_mapper()
|
||||
types = const.AppType.type_category_mapper().keys()
|
||||
|
||||
for tp in types:
|
||||
if not settings.XPACK_ENABLED and const.AppType.is_xpack(tp):
|
||||
continue
|
||||
|
@ -142,7 +145,6 @@ class ApplicationTreeNodeMixin:
|
|||
pid, counts, show_empty=show_empty,
|
||||
show_count=show_count
|
||||
)
|
||||
|
||||
return tree_nodes
|
||||
|
||||
@classmethod
|
||||
|
@ -171,13 +173,18 @@ class ApplicationTreeNodeMixin:
|
|||
pid = self.create_tree_id(pid, 'type', self.type)
|
||||
return pid
|
||||
|
||||
def as_tree_node(self, pid, is_luna=False):
|
||||
if is_luna and self.type == const.AppType.k8s:
|
||||
def as_tree_node(self, pid, k8s_as_tree=False):
|
||||
if self.type == const.AppType.k8s and k8s_as_tree:
|
||||
node = KubernetesTree(pid).as_tree_node(self)
|
||||
else:
|
||||
node = self._as_tree_node(pid)
|
||||
return node
|
||||
|
||||
def _attrs_to_tree(self):
|
||||
if self.category == const.AppCategory.db:
|
||||
return self.attrs
|
||||
return {}
|
||||
|
||||
def _as_tree_node(self, pid):
|
||||
icon_skin_category_mapper = {
|
||||
'remote_app': 'chrome',
|
||||
|
@ -199,6 +206,7 @@ class ApplicationTreeNodeMixin:
|
|||
'data': {
|
||||
'category': self.category,
|
||||
'type': self.type,
|
||||
'attrs': self._attrs_to_tree()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -239,6 +247,14 @@ class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin):
|
|||
def category_remote_app(self):
|
||||
return self.category == const.AppCategory.remote_app.value
|
||||
|
||||
@property
|
||||
def category_cloud(self):
|
||||
return self.category == const.AppCategory.cloud.value
|
||||
|
||||
@property
|
||||
def category_db(self):
|
||||
return self.category == const.AppCategory.db.value
|
||||
|
||||
def get_rdp_remote_app_setting(self):
|
||||
from applications.serializers.attrs import get_serializer_class_by_application_type
|
||||
if not self.category_remote_app:
|
||||
|
@ -264,12 +280,24 @@ class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin):
|
|||
'parameters': parameters
|
||||
}
|
||||
|
||||
def get_remote_app_asset(self):
|
||||
def get_remote_app_asset(self, raise_exception=True):
|
||||
asset_id = self.attrs.get('asset')
|
||||
if not asset_id:
|
||||
if is_uuid(asset_id):
|
||||
return Asset.objects.filter(id=asset_id).first()
|
||||
if raise_exception:
|
||||
raise ValueError("Remote App not has asset attr")
|
||||
asset = Asset.objects.filter(id=asset_id).first()
|
||||
return asset
|
||||
|
||||
def get_target_ip(self):
|
||||
if self.category_remote_app:
|
||||
asset = self.get_remote_app_asset()
|
||||
target_ip = asset.ip
|
||||
elif self.category_cloud:
|
||||
target_ip = self.attrs.get('cluster')
|
||||
elif self.category_db:
|
||||
target_ip = self.attrs.get('host')
|
||||
else:
|
||||
target_ip = ''
|
||||
return target_ip
|
||||
|
||||
|
||||
class ApplicationUser(SystemUser):
|
||||
|
|
|
@ -16,7 +16,7 @@ from perms.filters import AssetPermissionFilter
|
|||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from orgs.mixins import generics
|
||||
from assets.api import FilterAssetByNodeMixin
|
||||
from ..models import Asset, Node, Platform
|
||||
from ..models import Asset, Node, Platform, Gateway
|
||||
from .. import serializers
|
||||
from ..tasks import (
|
||||
update_assets_hardware_info_manual, test_assets_connectivity_manual,
|
||||
|
@ -181,7 +181,7 @@ class AssetsTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
|
|||
def check_permissions(self, request):
|
||||
action = request.data.get('action')
|
||||
action_perm_require = {
|
||||
'refresh': 'assets.refresh_assethardwareinfo1',
|
||||
'refresh': 'assets.refresh_assethardwareinfo',
|
||||
}
|
||||
perm_required = action_perm_require.get(action)
|
||||
has = self.request.user.has_perm(perm_required)
|
||||
|
@ -199,7 +199,7 @@ class AssetGatewayListApi(generics.ListAPIView):
|
|||
asset_id = self.kwargs.get('pk')
|
||||
asset = get_object_or_404(Asset, pk=asset_id)
|
||||
if not asset.domain:
|
||||
return []
|
||||
return Gateway.objects.none()
|
||||
queryset = asset.domain.gateways.filter(protocol='ssh')
|
||||
return queryset
|
||||
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.1.14 on 2022-04-12 03:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0089_auto_20220310_0616'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='number',
|
||||
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Asset number'),
|
||||
),
|
||||
]
|
|
@ -223,7 +223,7 @@ class Asset(AbsConnectivity, AbsHardwareInfo, ProtocolsMixin, NodesRelationMixin
|
|||
|
||||
# Some information
|
||||
public_ip = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Public IP'))
|
||||
number = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Asset number'))
|
||||
number = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Asset number'))
|
||||
|
||||
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'))
|
||||
|
@ -235,6 +235,9 @@ class Asset(AbsConnectivity, AbsHardwareInfo, ProtocolsMixin, NodesRelationMixin
|
|||
def __str__(self):
|
||||
return '{0.hostname}({0.ip})'.format(self)
|
||||
|
||||
def get_target_ip(self):
|
||||
return self.ip
|
||||
|
||||
def set_admin_user_relation(self):
|
||||
from .authbook import AuthBook
|
||||
if not self.admin_user:
|
||||
|
@ -280,16 +283,44 @@ class Asset(AbsConnectivity, AbsHardwareInfo, ProtocolsMixin, NodesRelationMixin
|
|||
def is_support_ansible(self):
|
||||
return self.has_protocol('ssh') and self.platform_base not in ("Other",)
|
||||
|
||||
def get_auth_info(self):
|
||||
def get_auth_info(self, with_become=False):
|
||||
if not self.admin_user:
|
||||
return {}
|
||||
|
||||
self.admin_user.load_asset_special_auth(self)
|
||||
if self.is_unixlike() and self.admin_user.su_enabled and self.admin_user.su_from:
|
||||
auth_user = self.admin_user.su_from
|
||||
become_user = self.admin_user
|
||||
else:
|
||||
auth_user = self.admin_user
|
||||
become_user = None
|
||||
|
||||
auth_user.load_asset_special_auth(self)
|
||||
info = {
|
||||
'username': self.admin_user.username,
|
||||
'password': self.admin_user.password,
|
||||
'private_key': self.admin_user.private_key_file,
|
||||
'username': auth_user.username,
|
||||
'password': auth_user.password,
|
||||
'private_key': auth_user.private_key_file
|
||||
}
|
||||
|
||||
if not with_become:
|
||||
return info
|
||||
|
||||
if become_user:
|
||||
become_user.load_asset_special_auth(self)
|
||||
become_method = 'su'
|
||||
become_username = become_user.username
|
||||
become_pass = become_user.password
|
||||
else:
|
||||
become_method = 'sudo'
|
||||
become_username = 'root'
|
||||
become_pass = auth_user.password
|
||||
become_info = {
|
||||
'become': {
|
||||
'method': become_method,
|
||||
'username': become_username,
|
||||
'pass': become_pass
|
||||
}
|
||||
}
|
||||
info.update(become_info)
|
||||
return info
|
||||
|
||||
def nodes_display(self):
|
||||
|
|
|
@ -133,6 +133,14 @@ class AuthMixin:
|
|||
self.password = password
|
||||
|
||||
def load_app_more_auth(self, app_id=None, username=None, user_id=None):
|
||||
from applications.models import Application
|
||||
app = get_object_or_none(Application, pk=app_id)
|
||||
if app and app.category_remote_app:
|
||||
# Remote app
|
||||
self._load_remoteapp_more_auth(app, username, user_id)
|
||||
return
|
||||
|
||||
# Other app
|
||||
self._clean_auth_info_if_manual_login_mode()
|
||||
# 加载临时认证信息
|
||||
if self.login_mode == self.LOGIN_MANUAL:
|
||||
|
@ -148,6 +156,11 @@ class AuthMixin:
|
|||
_username = username
|
||||
self.username = _username
|
||||
|
||||
def _load_remoteapp_more_auth(self, app, username, user_id):
|
||||
asset = app.get_remote_app_asset(raise_exception=False)
|
||||
if asset:
|
||||
self.load_asset_more_auth(asset_id=asset.id, username=username, user_id=user_id)
|
||||
|
||||
def load_asset_special_auth(self, asset, username=''):
|
||||
"""
|
||||
AuthBook 的数据状态
|
||||
|
|
|
@ -15,6 +15,7 @@ class AdminUserSerializer(SuS):
|
|||
SuS.Meta.fields_m2m + \
|
||||
[
|
||||
'type', 'protocol', "priority", 'sftp_root', 'ssh_key_fingerprint',
|
||||
'su_enabled', 'su_from',
|
||||
'date_created', 'date_updated', 'comment', 'created_by',
|
||||
]
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ class DomainSerializer(BulkOrgResourceModelSerializer):
|
|||
|
||||
|
||||
class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
is_connective = serializers.BooleanField(required=False)
|
||||
is_connective = serializers.BooleanField(required=False, label=_('Connectivity'))
|
||||
|
||||
class Meta:
|
||||
model = Gateway
|
||||
|
|
|
@ -70,6 +70,7 @@ class AuthBackendLabelMapping(LazyObject):
|
|||
backend_label_mapping[settings.AUTH_BACKEND_AUTH_TOKEN] = _('Auth Token')
|
||||
backend_label_mapping[settings.AUTH_BACKEND_WECOM] = _('WeCom')
|
||||
backend_label_mapping[settings.AUTH_BACKEND_DINGTALK] = _('DingTalk')
|
||||
backend_label_mapping[settings.AUTH_BACKEND_TEMP_TOKEN] = _('Temporary token')
|
||||
return backend_label_mapping
|
||||
|
||||
def _setup(self):
|
||||
|
|
|
@ -11,3 +11,4 @@ from .wecom import *
|
|||
from .dingtalk import *
|
||||
from .feishu import *
|
||||
from .password import *
|
||||
from .temp_token import *
|
||||
|
|
|
@ -8,7 +8,6 @@ from .. import serializers
|
|||
|
||||
|
||||
class AccessKeyViewSet(ModelViewSet):
|
||||
permission_classes = (IsValidUser,)
|
||||
serializer_class = serializers.AccessKeySerializer
|
||||
search_fields = ['^id', '^secret']
|
||||
|
||||
|
|
|
@ -30,13 +30,14 @@ from common.http import is_true
|
|||
from perms.models.base import Action
|
||||
from perms.utils.application.permission import get_application_actions
|
||||
from perms.utils.asset.permission import get_asset_actions
|
||||
|
||||
from common.const.http import PATCH
|
||||
from terminal.models import EndpointRule
|
||||
from ..serializers import (
|
||||
ConnectionTokenSerializer, ConnectionTokenSecretSerializer,
|
||||
)
|
||||
|
||||
logger = get_logger(__name__)
|
||||
__all__ = ['UserConnectionTokenViewSet']
|
||||
__all__ = ['UserConnectionTokenViewSet', 'TokenCacheMixin']
|
||||
|
||||
|
||||
class ClientProtocolMixin:
|
||||
|
@ -51,6 +52,17 @@ class ClientProtocolMixin:
|
|||
request: Request
|
||||
get_serializer: Callable
|
||||
create_token: Callable
|
||||
get_serializer_context: Callable
|
||||
|
||||
def get_smart_endpoint(self, protocol, asset=None, application=None):
|
||||
if asset:
|
||||
target_ip = asset.get_target_ip()
|
||||
elif application:
|
||||
target_ip = application.get_target_ip()
|
||||
else:
|
||||
target_ip = ''
|
||||
endpoint = EndpointRule.match_endpoint(target_ip, protocol, self.request)
|
||||
return endpoint
|
||||
|
||||
def get_request_resource(self, serializer):
|
||||
asset = serializer.validated_data.get('asset')
|
||||
|
@ -92,7 +104,7 @@ class ClientProtocolMixin:
|
|||
'autoreconnection enabled:i': '1',
|
||||
'bookmarktype:i': '3',
|
||||
'use redirection server name:i': '0',
|
||||
'smart sizing:i': '0',
|
||||
'smart sizing:i': '1',
|
||||
#'drivestoredirect:s': '*',
|
||||
# 'domain:s': ''
|
||||
# 'alternate shell:s:': '||MySQLWorkbench',
|
||||
|
@ -122,10 +134,10 @@ class ClientProtocolMixin:
|
|||
options['screen mode id:i'] = '2' if full_screen else '1'
|
||||
|
||||
# RDP Server 地址
|
||||
address = settings.TERMINAL_RDP_ADDR
|
||||
if not address or address == 'localhost:3389':
|
||||
address = self.request.get_host().split(':')[0] + ':3389'
|
||||
options['full address:s'] = address
|
||||
endpoint = self.get_smart_endpoint(
|
||||
protocol='rdp', asset=asset, application=application
|
||||
)
|
||||
options['full address:s'] = f'{endpoint.host}:{endpoint.rdp_port}'
|
||||
# 用户名
|
||||
options['username:s'] = '{}|{}'.format(user.username, token)
|
||||
if system_user.ad_domain:
|
||||
|
@ -134,8 +146,7 @@ class ClientProtocolMixin:
|
|||
if width and height:
|
||||
options['desktopwidth:i'] = width
|
||||
options['desktopheight:i'] = height
|
||||
else:
|
||||
options['smart sizing:i'] = '1'
|
||||
options['winposstr:s:'] = f'0,1,0,0,{width},{height}'
|
||||
|
||||
options['session bpp:i'] = os.getenv('JUMPSERVER_COLOR_DEPTH', '32')
|
||||
options['audiomode:i'] = self.parse_env_bool('JUMPSERVER_DISABLE_AUDIO', 'false', '2', '0')
|
||||
|
@ -160,6 +171,28 @@ class ClientProtocolMixin:
|
|||
content += f'{k}:{v}\n'
|
||||
return name, content
|
||||
|
||||
def get_ssh_token(self, serializer):
|
||||
asset, application, system_user, user = self.get_request_resource(serializer)
|
||||
token, secret = self.create_token(user, asset, application, system_user)
|
||||
if asset:
|
||||
name = asset.hostname
|
||||
elif application:
|
||||
name = application.name
|
||||
else:
|
||||
name = '*'
|
||||
|
||||
endpoint = self.get_smart_endpoint(
|
||||
protocol='ssh', asset=asset, application=application
|
||||
)
|
||||
content = {
|
||||
'ip': endpoint.host,
|
||||
'port': str(endpoint.ssh_port),
|
||||
'username': f'JMS-{token}',
|
||||
'password': secret
|
||||
}
|
||||
token = json.dumps(content)
|
||||
return name, token
|
||||
|
||||
def get_encrypt_cmdline(self, app: Application):
|
||||
parameters = app.get_rdp_remote_app_setting()['parameters']
|
||||
parameters = parameters.encode('ascii')
|
||||
|
@ -201,13 +234,11 @@ class ClientProtocolMixin:
|
|||
asset, application, system_user, user = self.get_request_resource(serializer)
|
||||
protocol = system_user.protocol
|
||||
username = user.username
|
||||
|
||||
config, token = '', ''
|
||||
if protocol == 'rdp':
|
||||
name, config = self.get_rdp_file_content(serializer)
|
||||
elif protocol == 'ssh':
|
||||
# Todo:
|
||||
name = ''
|
||||
config = 'ssh://system_user@asset@user@jumpserver-ssh'
|
||||
name, token = self.get_ssh_token(serializer)
|
||||
else:
|
||||
raise ValueError('Protocol not support: {}'.format(protocol))
|
||||
|
||||
|
@ -216,6 +247,7 @@ class ClientProtocolMixin:
|
|||
"filename": filename,
|
||||
"protocol": system_user.protocol,
|
||||
"username": username,
|
||||
"token": token,
|
||||
"config": config
|
||||
}
|
||||
return data
|
||||
|
@ -327,18 +359,56 @@ class SecretDetailMixin:
|
|||
return Response(data=serializer.data, status=200)
|
||||
|
||||
|
||||
class TokenCacheMixin:
|
||||
""" endpoint smart view 用到此类来解析token中的资产、应用 """
|
||||
CACHE_KEY_PREFIX = 'CONNECTION_TOKEN_{}'
|
||||
|
||||
def get_token_cache_key(self, token):
|
||||
return self.CACHE_KEY_PREFIX.format(token)
|
||||
|
||||
def get_token_ttl(self, token):
|
||||
key = self.get_token_cache_key(token)
|
||||
return cache.ttl(key)
|
||||
|
||||
def set_token_to_cache(self, token, value, ttl=5*60):
|
||||
key = self.get_token_cache_key(token)
|
||||
cache.set(key, value, timeout=ttl)
|
||||
|
||||
def get_token_from_cache(self, token):
|
||||
key = self.get_token_cache_key(token)
|
||||
value = cache.get(key, None)
|
||||
return value
|
||||
|
||||
def renewal_token(self, token, ttl=5*60):
|
||||
value = self.get_token_from_cache(token)
|
||||
if value:
|
||||
pre_ttl = self.get_token_ttl(token)
|
||||
self.set_token_to_cache(token, value, ttl)
|
||||
post_ttl = self.get_token_ttl(token)
|
||||
ok = True
|
||||
msg = f'{pre_ttl}s is renewed to {post_ttl}s.'
|
||||
else:
|
||||
ok = False
|
||||
msg = 'Token is not found.'
|
||||
data = {
|
||||
'ok': ok,
|
||||
'msg': msg
|
||||
}
|
||||
return data
|
||||
|
||||
|
||||
class UserConnectionTokenViewSet(
|
||||
RootOrgViewMixin, SerializerMixin, ClientProtocolMixin,
|
||||
SecretDetailMixin, GenericViewSet
|
||||
SecretDetailMixin, TokenCacheMixin, GenericViewSet
|
||||
):
|
||||
serializer_classes = {
|
||||
'default': ConnectionTokenSerializer,
|
||||
'get_secret_detail': ConnectionTokenSecretSerializer,
|
||||
}
|
||||
CACHE_KEY_PREFIX = 'CONNECTION_TOKEN_{}'
|
||||
rbac_perms = {
|
||||
'GET': 'authentication.view_connectiontoken',
|
||||
'create': 'authentication.add_connectiontoken',
|
||||
'renewal': 'authentication.add_superconnectiontoken',
|
||||
'get_secret_detail': 'authentication.view_connectiontokensecret',
|
||||
'get_rdp_file': 'authentication.add_connectiontoken',
|
||||
'get_client_protocol_url': 'authentication.add_connectiontoken',
|
||||
|
@ -359,6 +429,17 @@ class UserConnectionTokenViewSet(
|
|||
raise PermissionDenied(error)
|
||||
return True
|
||||
|
||||
@action(methods=[PATCH], detail=False)
|
||||
def renewal(self, request, *args, **kwargs):
|
||||
""" 续期 Token """
|
||||
perm_required = 'authentication.add_superconnectiontoken'
|
||||
if not request.user.has_perm(perm_required):
|
||||
raise PermissionDenied('No permissions for authentication.add_superconnectiontoken')
|
||||
token = request.data.get('token', '')
|
||||
data = self.renewal_token(token)
|
||||
status_code = 200 if data.get('ok') else 404
|
||||
return Response(data=data, status=status_code)
|
||||
|
||||
def create_token(self, user, asset, application, system_user, ttl=5*60):
|
||||
# 再次强调一下权限
|
||||
perm_required = 'authentication.add_superconnectiontoken'
|
||||
|
@ -391,8 +472,7 @@ class UserConnectionTokenViewSet(
|
|||
'application_name': str(application)
|
||||
})
|
||||
|
||||
key = self.CACHE_KEY_PREFIX.format(token)
|
||||
cache.set(key, value, timeout=ttl)
|
||||
self.set_token_to_cache(token, value, ttl)
|
||||
return token, secret
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
|
@ -415,8 +495,7 @@ class UserConnectionTokenViewSet(
|
|||
from perms.utils.asset.permission import validate_permission as asset_validate_permission
|
||||
from perms.utils.application.permission import validate_permission as app_validate_permission
|
||||
|
||||
key = self.CACHE_KEY_PREFIX.format(token)
|
||||
value = cache.get(key, None)
|
||||
value = self.get_token_from_cache(token)
|
||||
if not value:
|
||||
raise serializers.ValidationError('Token not found')
|
||||
|
||||
|
@ -442,9 +521,7 @@ class UserConnectionTokenViewSet(
|
|||
|
||||
def get(self, request):
|
||||
token = request.query_params.get('token')
|
||||
key = self.CACHE_KEY_PREFIX.format(token)
|
||||
value = cache.get(key, None)
|
||||
|
||||
value = self.get_token_from_cache(token)
|
||||
if not value:
|
||||
return Response('', status=404)
|
||||
return Response(value)
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
from django.utils import timezone
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.decorators import action
|
||||
|
||||
from common.drf.api import JMSModelViewSet
|
||||
from common.permissions import IsValidUser
|
||||
from ..models import TempToken
|
||||
from ..serializers import TempTokenSerializer
|
||||
|
||||
|
||||
class TempTokenViewSet(JMSModelViewSet):
|
||||
serializer_class = TempTokenSerializer
|
||||
permission_classes = [IsValidUser]
|
||||
http_method_names = ['post', 'get', 'options', 'patch']
|
||||
|
||||
def get_queryset(self):
|
||||
username = self.request.user.username
|
||||
return TempToken.objects.filter(username=username)
|
||||
|
||||
@action(methods=['PATCH'], detail=True, url_path='expire')
|
||||
def expire(self, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
instance.date_expired = timezone.now()
|
||||
instance.save()
|
||||
serializer = self.get_serializer(instance)
|
||||
return Response(serializer.data)
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.shortcuts import redirect
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.generics import CreateAPIView
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
from django.contrib.auth.backends import BaseBackend
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from users.models import User
|
||||
from common.utils import get_logger
|
||||
|
||||
|
||||
UserModel = get_user_model()
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ class LDAPAuthorizationBackend(JMSBaseAuthBackend, LDAPBackend):
|
|||
else:
|
||||
built = False
|
||||
|
||||
return (user, built)
|
||||
return user, built
|
||||
|
||||
def pre_check(self, username, password):
|
||||
if not settings.AUTH_LDAP:
|
||||
|
@ -75,6 +75,9 @@ class LDAPAuthorizationBackend(JMSBaseAuthBackend, LDAPBackend):
|
|||
|
||||
def authenticate(self, request=None, username=None, password=None, **kwargs):
|
||||
logger.info('Authentication LDAP backend')
|
||||
if username is None or password is None:
|
||||
logger.info('No username or password')
|
||||
return None
|
||||
match, msg = self.pre_check(username, password)
|
||||
if not match:
|
||||
logger.info('Authenticate failed: {}'.format(msg))
|
||||
|
|
|
@ -13,17 +13,20 @@ User = get_user_model()
|
|||
|
||||
|
||||
class CreateUserMixin:
|
||||
def get_django_user(self, username, password=None, *args, **kwargs):
|
||||
@staticmethod
|
||||
def get_django_user(username, password=None, *args, **kwargs):
|
||||
if isinstance(username, bytes):
|
||||
username = username.decode()
|
||||
try:
|
||||
user = User.objects.get(username=username)
|
||||
except User.DoesNotExist:
|
||||
user = User.objects.filter(username=username).first()
|
||||
if user:
|
||||
return user
|
||||
|
||||
if '@' in username:
|
||||
email = username
|
||||
else:
|
||||
email_suffix = settings.EMAIL_SUFFIX
|
||||
email = '{}@{}'.format(username, email_suffix)
|
||||
|
||||
user = User(username=username, name=username, email=email)
|
||||
user.source = user.Source.radius.value
|
||||
user.save()
|
||||
|
|
|
@ -14,7 +14,7 @@ from ..base import JMSModelBackend
|
|||
|
||||
__all__ = ['SAML2Backend']
|
||||
|
||||
logger = get_logger(__file__)
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class SAML2Backend(JMSModelBackend):
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import PermissionDenied
|
||||
|
||||
from authentication.models import TempToken
|
||||
from .base import JMSModelBackend
|
||||
|
||||
|
||||
class TempTokenAuthBackend(JMSModelBackend):
|
||||
model = TempToken
|
||||
|
||||
def authenticate(self, request, username='', password='', *args, **kwargs):
|
||||
token = self.model.objects.filter(username=username, secret=password).first()
|
||||
if not token:
|
||||
return None
|
||||
if not token.is_valid:
|
||||
raise PermissionDenied('Token is invalid, expired at {}'.format(token.date_expired))
|
||||
|
||||
token.verified = True
|
||||
token.date_verified = timezone.now()
|
||||
token.save()
|
||||
return token.user
|
||||
|
||||
@staticmethod
|
||||
def is_enabled():
|
||||
return settings.AUTH_TEMP_TOKEN
|
|
@ -0,0 +1,32 @@
|
|||
# Generated by Django 3.1.14 on 2022-04-08 07:04
|
||||
|
||||
from django.db import migrations, models
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('authentication', '0009_auto_20220310_0616'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='TempToken',
|
||||
fields=[
|
||||
('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')),
|
||||
('updated_by', models.CharField(blank=True, max_length=32, 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')),
|
||||
('username', models.CharField(max_length=128, verbose_name='Username')),
|
||||
('secret', models.CharField(max_length=64, verbose_name='Secret')),
|
||||
('verified', models.BooleanField(default=False, verbose_name='Verified')),
|
||||
('date_verified', models.DateTimeField(null=True, verbose_name='Date verified')),
|
||||
('date_expired', models.DateTimeField(verbose_name='Date verified')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Temporary token',
|
||||
},
|
||||
),
|
||||
]
|
|
@ -1,8 +1,9 @@
|
|||
import uuid
|
||||
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework.authtoken.models import Token
|
||||
from django.conf import settings
|
||||
from rest_framework.authtoken.models import Token
|
||||
|
||||
from common.db import models
|
||||
|
||||
|
@ -64,6 +65,27 @@ class ConnectionToken(models.JMSBaseModel):
|
|||
]
|
||||
|
||||
|
||||
class TempToken(models.JMSModel):
|
||||
username = models.CharField(max_length=128, verbose_name=_("Username"))
|
||||
secret = models.CharField(max_length=64, verbose_name=_("Secret"))
|
||||
verified = models.BooleanField(default=False, verbose_name=_("Verified"))
|
||||
date_verified = models.DateTimeField(null=True, verbose_name=_("Date verified"))
|
||||
date_expired = models.DateTimeField(verbose_name=_("Date expired"))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Temporary token")
|
||||
|
||||
@property
|
||||
def user(self):
|
||||
from users.models import User
|
||||
return User.objects.filter(username=self.username).first()
|
||||
|
||||
@property
|
||||
def is_valid(self):
|
||||
not_expired = self.date_expired and self.date_expired > timezone.now()
|
||||
return not self.verified and not_expired
|
||||
|
||||
|
||||
class SuperConnectionToken(ConnectionToken):
|
||||
class Meta:
|
||||
proxy = True
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
from .token import *
|
||||
from .connect_token import *
|
||||
from .password_mfa import *
|
|
@ -1,109 +1,22 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.utils import timezone
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.utils import get_object_or_none
|
||||
from users.models import User
|
||||
from assets.models import Asset, SystemUser, Gateway, Domain, CommandFilterRule
|
||||
from applications.models import Application
|
||||
from users.serializers import UserProfileSerializer
|
||||
from assets.serializers import ProtocolsField
|
||||
from perms.serializers.base import ActionsField
|
||||
from .models import AccessKey
|
||||
|
||||
__all__ = [
|
||||
'AccessKeySerializer', 'OtpVerifySerializer', 'BearerTokenSerializer',
|
||||
'MFAChallengeSerializer', 'SSOTokenSerializer',
|
||||
'ConnectionTokenSerializer', 'ConnectionTokenSecretSerializer',
|
||||
'PasswordVerifySerializer', 'MFASelectTypeSerializer',
|
||||
'ConnectionTokenSerializer', 'ConnectionTokenApplicationSerializer',
|
||||
'ConnectionTokenUserSerializer', 'ConnectionTokenFilterRuleSerializer',
|
||||
'ConnectionTokenAssetSerializer', 'ConnectionTokenSystemUserSerializer',
|
||||
'ConnectionTokenDomainSerializer', 'ConnectionTokenRemoteAppSerializer',
|
||||
'ConnectionTokenGatewaySerializer', 'ConnectionTokenSecretSerializer'
|
||||
]
|
||||
|
||||
|
||||
class AccessKeySerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = AccessKey
|
||||
fields = ['id', 'secret', 'is_active', 'date_created']
|
||||
read_only_fields = ['id', 'secret', 'date_created']
|
||||
|
||||
|
||||
class OtpVerifySerializer(serializers.Serializer):
|
||||
code = serializers.CharField(max_length=6, min_length=6)
|
||||
|
||||
|
||||
class PasswordVerifySerializer(serializers.Serializer):
|
||||
password = serializers.CharField()
|
||||
|
||||
|
||||
class BearerTokenSerializer(serializers.Serializer):
|
||||
username = serializers.CharField(allow_null=True, required=False, write_only=True)
|
||||
password = serializers.CharField(write_only=True, allow_null=True,
|
||||
required=False, allow_blank=True)
|
||||
public_key = serializers.CharField(write_only=True, allow_null=True,
|
||||
allow_blank=True, required=False)
|
||||
token = serializers.CharField(read_only=True)
|
||||
keyword = serializers.SerializerMethodField()
|
||||
date_expired = serializers.DateTimeField(read_only=True)
|
||||
user = UserProfileSerializer(read_only=True)
|
||||
|
||||
@staticmethod
|
||||
def get_keyword(obj):
|
||||
return 'Bearer'
|
||||
|
||||
def update_last_login(self, user):
|
||||
user.last_login = timezone.now()
|
||||
user.save(update_fields=['last_login'])
|
||||
|
||||
def get_request_user(self):
|
||||
request = self.context.get('request')
|
||||
if request.user and request.user.is_authenticated:
|
||||
user = request.user
|
||||
else:
|
||||
user_id = request.session.get('user_id')
|
||||
user = get_object_or_none(User, pk=user_id)
|
||||
if not user:
|
||||
raise serializers.ValidationError(
|
||||
"user id {} not exist".format(user_id)
|
||||
)
|
||||
return user
|
||||
|
||||
def create(self, validated_data):
|
||||
request = self.context.get('request')
|
||||
user = self.get_request_user()
|
||||
|
||||
token, date_expired = user.create_bearer_token(request)
|
||||
self.update_last_login(user)
|
||||
|
||||
instance = {
|
||||
"token": token,
|
||||
"date_expired": date_expired,
|
||||
"user": user
|
||||
}
|
||||
return instance
|
||||
|
||||
|
||||
class MFASelectTypeSerializer(serializers.Serializer):
|
||||
type = serializers.CharField()
|
||||
username = serializers.CharField(required=False, allow_blank=True, allow_null=True)
|
||||
|
||||
|
||||
class MFAChallengeSerializer(serializers.Serializer):
|
||||
type = serializers.CharField(write_only=True, required=False, allow_blank=True)
|
||||
code = serializers.CharField(write_only=True)
|
||||
|
||||
def create(self, validated_data):
|
||||
pass
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
pass
|
||||
|
||||
|
||||
class SSOTokenSerializer(serializers.Serializer):
|
||||
username = serializers.CharField(write_only=True)
|
||||
login_url = serializers.CharField(read_only=True)
|
||||
next = serializers.CharField(write_only=True, allow_blank=True, required=False, allow_null=True)
|
||||
|
||||
|
||||
class ConnectionTokenSerializer(serializers.Serializer):
|
||||
user = serializers.CharField(max_length=128, required=False, allow_blank=True)
|
||||
system_user = serializers.CharField(max_length=128, required=True)
|
|
@ -0,0 +1,33 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
__all__ = [
|
||||
'OtpVerifySerializer', 'MFAChallengeSerializer', 'MFASelectTypeSerializer',
|
||||
'PasswordVerifySerializer',
|
||||
]
|
||||
|
||||
|
||||
class PasswordVerifySerializer(serializers.Serializer):
|
||||
password = serializers.CharField()
|
||||
|
||||
|
||||
class MFASelectTypeSerializer(serializers.Serializer):
|
||||
type = serializers.CharField()
|
||||
username = serializers.CharField(required=False, allow_blank=True, allow_null=True)
|
||||
|
||||
|
||||
class MFAChallengeSerializer(serializers.Serializer):
|
||||
type = serializers.CharField(write_only=True, required=False, allow_blank=True)
|
||||
code = serializers.CharField(write_only=True)
|
||||
|
||||
def create(self, validated_data):
|
||||
pass
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
pass
|
||||
|
||||
|
||||
class OtpVerifySerializer(serializers.Serializer):
|
||||
code = serializers.CharField(max_length=6, min_length=6)
|
|
@ -0,0 +1,103 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.utils import get_object_or_none, random_string
|
||||
from users.models import User
|
||||
from users.serializers import UserProfileSerializer
|
||||
from ..models import AccessKey, TempToken
|
||||
|
||||
__all__ = [
|
||||
'AccessKeySerializer', 'BearerTokenSerializer',
|
||||
'SSOTokenSerializer', 'TempTokenSerializer',
|
||||
]
|
||||
|
||||
|
||||
class AccessKeySerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = AccessKey
|
||||
fields = ['id', 'secret', 'is_active', 'date_created']
|
||||
read_only_fields = ['id', 'secret', 'date_created']
|
||||
|
||||
|
||||
class BearerTokenSerializer(serializers.Serializer):
|
||||
username = serializers.CharField(allow_null=True, required=False, write_only=True)
|
||||
password = serializers.CharField(write_only=True, allow_null=True,
|
||||
required=False, allow_blank=True)
|
||||
public_key = serializers.CharField(write_only=True, allow_null=True,
|
||||
allow_blank=True, required=False)
|
||||
token = serializers.CharField(read_only=True)
|
||||
keyword = serializers.SerializerMethodField()
|
||||
date_expired = serializers.DateTimeField(read_only=True)
|
||||
user = UserProfileSerializer(read_only=True)
|
||||
|
||||
@staticmethod
|
||||
def get_keyword(obj):
|
||||
return 'Bearer'
|
||||
|
||||
def update_last_login(self, user):
|
||||
user.last_login = timezone.now()
|
||||
user.save(update_fields=['last_login'])
|
||||
|
||||
def get_request_user(self):
|
||||
request = self.context.get('request')
|
||||
if request.user and request.user.is_authenticated:
|
||||
user = request.user
|
||||
else:
|
||||
user_id = request.session.get('user_id')
|
||||
user = get_object_or_none(User, pk=user_id)
|
||||
if not user:
|
||||
raise serializers.ValidationError(
|
||||
"user id {} not exist".format(user_id)
|
||||
)
|
||||
return user
|
||||
|
||||
def create(self, validated_data):
|
||||
request = self.context.get('request')
|
||||
user = self.get_request_user()
|
||||
|
||||
token, date_expired = user.create_bearer_token(request)
|
||||
self.update_last_login(user)
|
||||
|
||||
instance = {
|
||||
"token": token,
|
||||
"date_expired": date_expired,
|
||||
"user": user
|
||||
}
|
||||
return instance
|
||||
|
||||
|
||||
class SSOTokenSerializer(serializers.Serializer):
|
||||
username = serializers.CharField(write_only=True)
|
||||
login_url = serializers.CharField(read_only=True)
|
||||
next = serializers.CharField(write_only=True, allow_blank=True, required=False, allow_null=True)
|
||||
|
||||
|
||||
class TempTokenSerializer(serializers.ModelSerializer):
|
||||
is_valid = serializers.BooleanField(label=_("Is valid"), read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = TempToken
|
||||
fields = [
|
||||
'id', 'username', 'secret', 'verified', 'is_valid',
|
||||
'date_created', 'date_updated', 'date_verified',
|
||||
'date_expired',
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
def create(self, validated_data):
|
||||
request = self.context.get('request')
|
||||
if not request or not request.user:
|
||||
raise PermissionError()
|
||||
|
||||
secret = random_string(36)
|
||||
username = request.user.username
|
||||
kwargs = {
|
||||
'username': username, 'secret': secret,
|
||||
'date_expired': timezone.now() + timezone.timedelta(seconds=5*60),
|
||||
}
|
||||
token = TempToken(**kwargs)
|
||||
token.save()
|
||||
return token
|
|
@ -142,15 +142,18 @@
|
|||
<li class="dropdown">
|
||||
<a class="dropdown-toggle login-page-language" data-toggle="dropdown" href="#" target="_blank">
|
||||
<i class="fa fa-globe fa-lg" style="margin-right: 2px"></i>
|
||||
{% ifequal request.COOKIES.django_language 'en' %}
|
||||
{% if request.COOKIES.django_language == 'en' %}
|
||||
<span>English<b class="caret"></b></span>
|
||||
{% elif request.COOKIES.django_language == 'ja' %}
|
||||
<span>日本語<b class="caret"></b></span>
|
||||
{% else %}
|
||||
<span>中文(简体)<b class="caret"></b></span>
|
||||
{% endifequal %}
|
||||
{% endif %}
|
||||
</a>
|
||||
<ul class="dropdown-menu profile-dropdown dropdown-menu-right">
|
||||
<li> <a id="switch_cn" href="{% url 'i18n-switch' lang='zh-hans' %}"> <span>中文(简体)</span> </a> </li>
|
||||
<li> <a id="switch_en" href="{% url 'i18n-switch' lang='en' %}"> <span>English</span> </a> </li>
|
||||
<li> <a id="switch_ja" href="{% url 'i18n-switch' lang='ja' %}"> <span>日本語</span> </a> </li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
@ -9,6 +9,7 @@ app_name = 'authentication'
|
|||
router = DefaultRouter()
|
||||
router.register('access-keys', api.AccessKeyViewSet, 'access-key')
|
||||
router.register('sso', api.SSOViewSet, 'sso')
|
||||
router.register('temp-tokens', api.TempTokenViewSet, 'temp-token')
|
||||
router.register('connection-token', api.UserConnectionTokenViewSet, 'connection-token')
|
||||
|
||||
|
||||
|
|
|
@ -27,12 +27,16 @@ urlpatterns = [
|
|||
path('wecom/qr/login/', views.WeComQRLoginView.as_view(), name='wecom-qr-login'),
|
||||
path('wecom/qr/bind/<uuid:user_id>/callback/', views.WeComQRBindCallbackView.as_view(), name='wecom-qr-bind-callback'),
|
||||
path('wecom/qr/login/callback/', views.WeComQRLoginCallbackView.as_view(), name='wecom-qr-login-callback'),
|
||||
path('wecom/oauth/login/', views.WeComOAuthLoginView.as_view(), name='wecom-oauth-login'),
|
||||
path('wecom/oauth/login/callback/', views.WeComOAuthLoginCallbackView.as_view(), name='wecom-oauth-login-callback'),
|
||||
|
||||
path('dingtalk/bind/start/', views.DingTalkEnableStartView.as_view(), name='dingtalk-bind-start'),
|
||||
path('dingtalk/qr/bind/', views.DingTalkQRBindView.as_view(), name='dingtalk-qr-bind'),
|
||||
path('dingtalk/qr/login/', views.DingTalkQRLoginView.as_view(), name='dingtalk-qr-login'),
|
||||
path('dingtalk/qr/bind/<uuid:user_id>/callback/', views.DingTalkQRBindCallbackView.as_view(), name='dingtalk-qr-bind-callback'),
|
||||
path('dingtalk/qr/login/callback/', views.DingTalkQRLoginCallbackView.as_view(), name='dingtalk-qr-login-callback'),
|
||||
path('dingtalk/oauth/login/', views.DingTalkOAuthLoginView.as_view(), name='dingtalk-oauth-login'),
|
||||
path('dingtalk/oauth/login/callback/', views.DingTalkOAuthLoginCallbackView.as_view(), name='dingtalk-oauth-login-callback'),
|
||||
|
||||
path('feishu/bind/start/', views.FeiShuEnableStartView.as_view(), name='feishu-bind-start'),
|
||||
path('feishu/qr/bind/', views.FeiShuQRBindView.as_view(), name='feishu-qr-bind'),
|
||||
|
@ -51,7 +55,6 @@ urlpatterns = [
|
|||
path('profile/otp/enable/bind/', users_view.UserOtpEnableBindView.as_view(), name='user-otp-enable-bind'),
|
||||
path('profile/otp/disable/', users_view.UserOtpDisableView.as_view(),
|
||||
name='user-otp-disable'),
|
||||
path('first-login/', users_view.UserFirstLoginView.as_view(), name='user-first-login'),
|
||||
|
||||
# openid
|
||||
path('cas/', include(('authentication.backends.cas.urls', 'authentication'), namespace='cas')),
|
||||
|
|
|
@ -9,8 +9,7 @@ from django.conf import settings
|
|||
from .notifications import DifferentCityLoginMessage
|
||||
from audits.models import UserLoginLog
|
||||
from audits.const import DEFAULT_CITY
|
||||
from common.utils import get_request_ip
|
||||
from common.utils import validate_ip, get_ip_city
|
||||
from common.utils import validate_ip, get_ip_city, get_request_ip
|
||||
from common.utils import get_logger
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
|
|
@ -28,7 +28,7 @@ logger = get_logger(__file__)
|
|||
DINGTALK_STATE_SESSION_KEY = '_dingtalk_state'
|
||||
|
||||
|
||||
class DingTalkQRMixin(PermissionsMixin, View):
|
||||
class DingTalkBaseMixin(PermissionsMixin, View):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
try:
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
@ -54,20 +54,6 @@ class DingTalkQRMixin(PermissionsMixin, View):
|
|||
msg = _("The system configuration is incorrect. Please contact your administrator")
|
||||
return self.get_failed_response(redirect_uri, msg, msg)
|
||||
|
||||
def get_qr_url(self, redirect_uri):
|
||||
state = random_string(16)
|
||||
self.request.session[DINGTALK_STATE_SESSION_KEY] = state
|
||||
|
||||
params = {
|
||||
'appid': settings.DINGTALK_APPKEY,
|
||||
'response_type': 'code',
|
||||
'scope': 'snsapi_login',
|
||||
'state': state,
|
||||
'redirect_uri': redirect_uri,
|
||||
}
|
||||
url = URL.QR_CONNECT + '?' + urlencode(params)
|
||||
return url
|
||||
|
||||
@staticmethod
|
||||
def get_success_response(redirect_url, title, msg):
|
||||
message_data = {
|
||||
|
@ -94,6 +80,42 @@ class DingTalkQRMixin(PermissionsMixin, View):
|
|||
return response
|
||||
|
||||
|
||||
class DingTalkQRMixin(DingTalkBaseMixin, View):
|
||||
|
||||
def get_qr_url(self, redirect_uri):
|
||||
state = random_string(16)
|
||||
self.request.session[DINGTALK_STATE_SESSION_KEY] = state
|
||||
|
||||
params = {
|
||||
'appid': settings.DINGTALK_APPKEY,
|
||||
'response_type': 'code',
|
||||
'scope': 'snsapi_login',
|
||||
'state': state,
|
||||
'redirect_uri': redirect_uri,
|
||||
}
|
||||
url = URL.QR_CONNECT + '?' + urlencode(params)
|
||||
return url
|
||||
|
||||
|
||||
class DingTalkOAuthMixin(DingTalkBaseMixin, View):
|
||||
|
||||
def get_oauth_url(self, redirect_uri):
|
||||
if not settings.AUTH_DINGTALK:
|
||||
return reverse('authentication:login')
|
||||
state = random_string(16)
|
||||
self.request.session[DINGTALK_STATE_SESSION_KEY] = state
|
||||
|
||||
params = {
|
||||
'appid': settings.DINGTALK_APPKEY,
|
||||
'response_type': 'code',
|
||||
'scope': 'snsapi_auth',
|
||||
'state': state,
|
||||
'redirect_uri': redirect_uri,
|
||||
}
|
||||
url = URL.OAUTH_CONNECT + '?' + urlencode(params)
|
||||
return url
|
||||
|
||||
|
||||
class DingTalkQRBindView(DingTalkQRMixin, View):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
||||
|
@ -230,3 +252,57 @@ class DingTalkQRLoginCallbackView(AuthMixin, DingTalkQRMixin, View):
|
|||
return response
|
||||
|
||||
return self.redirect_to_guard_view()
|
||||
|
||||
|
||||
class DingTalkOAuthLoginView(DingTalkOAuthMixin, View):
|
||||
permission_classes = (AllowAny,)
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
redirect_url = request.GET.get('redirect_url')
|
||||
|
||||
redirect_uri = reverse('authentication:dingtalk-oauth-login-callback', external=True)
|
||||
redirect_uri += '?' + urlencode({'redirect_url': redirect_url})
|
||||
|
||||
url = self.get_oauth_url(redirect_uri)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
class DingTalkOAuthLoginCallbackView(AuthMixin, DingTalkOAuthMixin, View):
|
||||
permission_classes = (AllowAny,)
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
code = request.GET.get('code')
|
||||
redirect_url = request.GET.get('redirect_url')
|
||||
login_url = reverse('authentication:login')
|
||||
|
||||
if not self.verify_state():
|
||||
return self.get_verify_state_failed_response(redirect_url)
|
||||
|
||||
dingtalk = DingTalk(
|
||||
appid=settings.DINGTALK_APPKEY,
|
||||
appsecret=settings.DINGTALK_APPSECRET,
|
||||
agentid=settings.DINGTALK_AGENTID
|
||||
)
|
||||
userid = dingtalk.get_userid_by_code(code)
|
||||
if not userid:
|
||||
# 正常流程不会出这个错误,hack 行为
|
||||
msg = _('Failed to get user from DingTalk')
|
||||
response = self.get_failed_response(login_url, title=msg, msg=msg)
|
||||
return response
|
||||
|
||||
user = get_object_or_none(User, dingtalk_id=userid)
|
||||
if user is None:
|
||||
title = _('DingTalk is not bound')
|
||||
msg = _('Please login with a password and then bind the DingTalk')
|
||||
response = self.get_failed_response(login_url, title=title, msg=msg)
|
||||
return response
|
||||
|
||||
try:
|
||||
self.check_oauth2_auth(user, settings.AUTH_BACKEND_DINGTALK)
|
||||
except errors.AuthFailedError as e:
|
||||
self.set_login_failed_mark()
|
||||
msg = e.msg
|
||||
response = self.get_failed_response(login_url, title=msg, msg=msg)
|
||||
return response
|
||||
|
||||
return self.redirect_to_guard_view()
|
|
@ -28,7 +28,7 @@ logger = get_logger(__file__)
|
|||
WECOM_STATE_SESSION_KEY = '_wecom_state'
|
||||
|
||||
|
||||
class WeComQRMixin(PermissionsMixin, View):
|
||||
class WeComBaseMixin(PermissionsMixin, View):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
try:
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
@ -54,19 +54,6 @@ class WeComQRMixin(PermissionsMixin, View):
|
|||
msg = _("The system configuration is incorrect. Please contact your administrator")
|
||||
return self.get_failed_response(redirect_uri, msg, msg)
|
||||
|
||||
def get_qr_url(self, redirect_uri):
|
||||
state = random_string(16)
|
||||
self.request.session[WECOM_STATE_SESSION_KEY] = state
|
||||
|
||||
params = {
|
||||
'appid': settings.WECOM_CORPID,
|
||||
'agentid': settings.WECOM_AGENTID,
|
||||
'state': state,
|
||||
'redirect_uri': redirect_uri,
|
||||
}
|
||||
url = URL.QR_CONNECT + '?' + urlencode(params)
|
||||
return url
|
||||
|
||||
@staticmethod
|
||||
def get_success_response(redirect_url, title, msg):
|
||||
message_data = {
|
||||
|
@ -93,6 +80,42 @@ class WeComQRMixin(PermissionsMixin, View):
|
|||
return response
|
||||
|
||||
|
||||
class WeComQRMixin(WeComBaseMixin, View):
|
||||
|
||||
def get_qr_url(self, redirect_uri):
|
||||
state = random_string(16)
|
||||
self.request.session[WECOM_STATE_SESSION_KEY] = state
|
||||
|
||||
params = {
|
||||
'appid': settings.WECOM_CORPID,
|
||||
'agentid': settings.WECOM_AGENTID,
|
||||
'state': state,
|
||||
'redirect_uri': redirect_uri,
|
||||
}
|
||||
url = URL.QR_CONNECT + '?' + urlencode(params)
|
||||
return url
|
||||
|
||||
|
||||
class WeComOAuthMixin(WeComBaseMixin, View):
|
||||
|
||||
def get_oauth_url(self, redirect_uri):
|
||||
if not settings.AUTH_WECOM:
|
||||
return reverse('authentication:login')
|
||||
state = random_string(16)
|
||||
self.request.session[WECOM_STATE_SESSION_KEY] = state
|
||||
|
||||
params = {
|
||||
'appid': settings.WECOM_CORPID,
|
||||
'agentid': settings.WECOM_AGENTID,
|
||||
'state': state,
|
||||
'redirect_uri': redirect_uri,
|
||||
'response_type': 'code',
|
||||
'scope': 'snsapi_base',
|
||||
}
|
||||
url = URL.OAUTH_CONNECT + '?' + urlencode(params) + '#wechat_redirect'
|
||||
return url
|
||||
|
||||
|
||||
class WeComQRBindView(WeComQRMixin, View):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
||||
|
@ -225,3 +248,57 @@ class WeComQRLoginCallbackView(AuthMixin, WeComQRMixin, View):
|
|||
return response
|
||||
|
||||
return self.redirect_to_guard_view()
|
||||
|
||||
|
||||
class WeComOAuthLoginView(WeComOAuthMixin, View):
|
||||
permission_classes = (AllowAny,)
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
redirect_url = request.GET.get('redirect_url')
|
||||
|
||||
redirect_uri = reverse('authentication:wecom-oauth-login-callback', external=True)
|
||||
redirect_uri += '?' + urlencode({'redirect_url': redirect_url})
|
||||
|
||||
url = self.get_oauth_url(redirect_uri)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
class WeComOAuthLoginCallbackView(AuthMixin, WeComOAuthMixin, View):
|
||||
permission_classes = (AllowAny,)
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
code = request.GET.get('code')
|
||||
redirect_url = request.GET.get('redirect_url')
|
||||
login_url = reverse('authentication:login')
|
||||
|
||||
if not self.verify_state():
|
||||
return self.get_verify_state_failed_response(redirect_url)
|
||||
|
||||
wecom = WeCom(
|
||||
corpid=settings.WECOM_CORPID,
|
||||
corpsecret=settings.WECOM_SECRET,
|
||||
agentid=settings.WECOM_AGENTID
|
||||
)
|
||||
wecom_userid, __ = wecom.get_user_id_by_code(code)
|
||||
if not wecom_userid:
|
||||
# 正常流程不会出这个错误,hack 行为
|
||||
msg = _('Failed to get user from WeCom')
|
||||
response = self.get_failed_response(login_url, title=msg, msg=msg)
|
||||
return response
|
||||
|
||||
user = get_object_or_none(User, wecom_id=wecom_userid)
|
||||
if user is None:
|
||||
title = _('WeCom is not bound')
|
||||
msg = _('Please login with a password and then bind the WeCom')
|
||||
response = self.get_failed_response(login_url, title=title, msg=msg)
|
||||
return response
|
||||
|
||||
try:
|
||||
self.check_oauth2_auth(user, settings.AUTH_BACKEND_WECOM)
|
||||
except errors.AuthFailedError as e:
|
||||
self.set_login_failed_mark()
|
||||
msg = e.msg
|
||||
response = self.get_failed_response(login_url, title=msg, msg=msg)
|
||||
return response
|
||||
|
||||
return self.redirect_to_guard_view()
|
|
@ -1,11 +1,9 @@
|
|||
import time
|
||||
|
||||
from redis import Redis
|
||||
|
||||
from common.utils.lock import DistributedLock
|
||||
from common.utils.connection import get_redis_client
|
||||
from common.utils import lazyproperty
|
||||
from common.utils import get_logger
|
||||
from jumpserver.const import CONFIG
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
@ -58,7 +56,7 @@ class Cache(metaclass=CacheType):
|
|||
|
||||
def __init__(self):
|
||||
self._data = None
|
||||
self.redis = Redis(host=CONFIG.REDIS_HOST, port=CONFIG.REDIS_PORT, password=CONFIG.REDIS_PASSWORD)
|
||||
self.redis = get_redis_client()
|
||||
|
||||
def __getitem__(self, item):
|
||||
return self.field_desc_mapper[item]
|
||||
|
|
|
@ -4,7 +4,7 @@ import json
|
|||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from ..utils import signer, crypto
|
||||
|
||||
|
||||
|
@ -13,7 +13,7 @@ __all__ = [
|
|||
'JsonCharField', 'JsonTextField', 'JsonListCharField', 'JsonListTextField',
|
||||
'JsonDictCharField', 'JsonDictTextField', 'EncryptCharField',
|
||||
'EncryptTextField', 'EncryptMixin', 'EncryptJsonDictTextField',
|
||||
'EncryptJsonDictCharField',
|
||||
'EncryptJsonDictCharField', 'PortField'
|
||||
]
|
||||
|
||||
|
||||
|
@ -180,3 +180,13 @@ class EncryptJsonDictTextField(EncryptMixin, JsonDictTextField):
|
|||
class EncryptJsonDictCharField(EncryptMixin, JsonDictCharField):
|
||||
pass
|
||||
|
||||
|
||||
class PortField(models.IntegerField):
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.update({
|
||||
'blank': False,
|
||||
'null': False,
|
||||
'validators': [MinValueValidator(0), MaxValueValidator(65535)]
|
||||
})
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ class ErrorCode:
|
|||
|
||||
class URL:
|
||||
QR_CONNECT = 'https://oapi.dingtalk.com/connect/qrconnect'
|
||||
OAUTH_CONNECT = 'https://oapi.dingtalk.com/connect/oauth2/sns_authorize'
|
||||
GET_USER_INFO_BY_CODE = 'https://oapi.dingtalk.com/sns/getuserinfo_bycode'
|
||||
GET_TOKEN = 'https://oapi.dingtalk.com/gettoken'
|
||||
SEND_MESSAGE_BY_TEMPLATE = 'https://oapi.dingtalk.com/topapi/message/corpconversation/sendbytemplate'
|
||||
|
|
|
@ -19,6 +19,7 @@ class URL:
|
|||
GET_TOKEN = 'https://qyapi.weixin.qq.com/cgi-bin/gettoken'
|
||||
SEND_MESSAGE = 'https://qyapi.weixin.qq.com/cgi-bin/message/send'
|
||||
QR_CONNECT = 'https://open.work.weixin.qq.com/wwopen/sso/qrConnect'
|
||||
OAUTH_CONNECT = 'https://open.weixin.qq.com/connect/oauth2/authorize'
|
||||
|
||||
# https://open.work.weixin.qq.com/api/doc/90000/90135/91437
|
||||
GET_USER_ID_BY_CODE = 'https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo'
|
||||
|
|
|
@ -31,6 +31,8 @@ def combine_seq(s1, s2, callback=None):
|
|||
|
||||
|
||||
def get_logger(name=''):
|
||||
if '/' in name:
|
||||
name = os.path.basename(name).replace('.py', '')
|
||||
return logging.getLogger('jumpserver.%s' % name)
|
||||
|
||||
|
||||
|
@ -338,3 +340,24 @@ def get_file_by_arch(dir, filename):
|
|||
settings.BASE_DIR, dir, platform_name, arch, filename
|
||||
)
|
||||
return file_path
|
||||
|
||||
|
||||
def pretty_string(data: str, max_length=128, ellipsis_str='...'):
|
||||
"""
|
||||
params:
|
||||
data: abcdefgh
|
||||
max_length: 7
|
||||
ellipsis_str: ...
|
||||
return:
|
||||
ab...gh
|
||||
"""
|
||||
if len(data) < max_length:
|
||||
return data
|
||||
remain_length = max_length - len(ellipsis_str)
|
||||
half = remain_length // 2
|
||||
if half <= 1:
|
||||
return data[:max_length]
|
||||
start = data[:half]
|
||||
end = data[-half:]
|
||||
data = f'{start}{ellipsis_str}{end}'
|
||||
return data
|
||||
|
|
|
@ -1,23 +1,30 @@
|
|||
import json
|
||||
import threading
|
||||
|
||||
import redis
|
||||
from redis import Redis
|
||||
from django.conf import settings
|
||||
|
||||
from jumpserver.const import CONFIG
|
||||
from common.http import is_true
|
||||
from common.db.utils import safe_db_connection
|
||||
from common.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def get_redis_client(db):
|
||||
rc = redis.StrictRedis(
|
||||
host=settings.REDIS_HOST,
|
||||
port=settings.REDIS_PORT,
|
||||
password=settings.REDIS_PASSWORD,
|
||||
db=db
|
||||
)
|
||||
return rc
|
||||
def get_redis_client(db=0):
|
||||
params = {
|
||||
'host': CONFIG.REDIS_HOST,
|
||||
'port': CONFIG.REDIS_PORT,
|
||||
'password': CONFIG.REDIS_PASSWORD,
|
||||
'db': db,
|
||||
"ssl": is_true(CONFIG.REDIS_USE_SSL),
|
||||
'ssl_cert_reqs': CONFIG.REDIS_SSL_REQUIRED,
|
||||
'ssl_keyfile': getattr(settings, 'REDIS_SSL_KEYFILE'),
|
||||
'ssl_certfile': getattr(settings, 'REDIS_SSL_CERTFILE'),
|
||||
'ssl_ca_certs': getattr(settings, 'REDIS_SSL_CA_CERTS'),
|
||||
}
|
||||
return Redis(**params)
|
||||
|
||||
|
||||
class Subscription:
|
||||
|
@ -99,5 +106,3 @@ class RedisPubSub:
|
|||
data_json = json.dumps(data)
|
||||
self.redis.publish(self.ch, data_json)
|
||||
return True
|
||||
|
||||
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import re
|
||||
|
||||
from django.shortcuts import reverse as dj_reverse
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.db import models
|
||||
from django.db.models.signals import post_save, pre_save
|
||||
|
||||
|
||||
UUID_PATTERN = re.compile(r'[0-9a-zA-Z\-]{36}')
|
||||
|
@ -58,3 +61,12 @@ def get_log_keep_day(s, defaults=200):
|
|||
except ValueError:
|
||||
days = defaults
|
||||
return days
|
||||
|
||||
|
||||
def bulk_create_with_signal(cls: models.Model, items, **kwargs):
|
||||
for i in items:
|
||||
pre_save.send(sender=cls, instance=i)
|
||||
result = cls.objects.bulk_create(items, **kwargs)
|
||||
for i in items:
|
||||
post_save.send(sender=cls, instance=i, created=True)
|
||||
return result
|
||||
|
|
|
@ -10,6 +10,7 @@ from django.db import transaction
|
|||
|
||||
from common.utils import get_logger
|
||||
from common.utils.inspect import copy_function_args
|
||||
from common.utils.connection import get_redis_client
|
||||
from jumpserver.const import CONFIG
|
||||
from common.local import thread_local
|
||||
|
||||
|
@ -44,7 +45,7 @@ class DistributedLock(RedisLock):
|
|||
是否可重入
|
||||
"""
|
||||
self.kwargs_copy = copy_function_args(self.__init__, locals())
|
||||
redis = Redis(host=CONFIG.REDIS_HOST, port=CONFIG.REDIS_PORT, password=CONFIG.REDIS_PASSWORD)
|
||||
redis = get_redis_client()
|
||||
|
||||
if expire is None:
|
||||
expire = auto_renewal_seconds
|
||||
|
|
|
@ -40,27 +40,3 @@ def random_string(length, lower=True, upper=True, digit=True, special_char=False
|
|||
|
||||
password = ''.join(password)
|
||||
return password
|
||||
|
||||
|
||||
# def strTimeProp(start, end, prop, fmt):
|
||||
# time_start = time.mktime(time.strptime(start, fmt))
|
||||
# time_end = time.mktime(time.strptime(end, fmt))
|
||||
# ptime = time_start + prop * (time_end - time_start)
|
||||
# return int(ptime)
|
||||
#
|
||||
#
|
||||
# def randomTimestamp(start, end, fmt='%Y-%m-%d %H:%M:%S'):
|
||||
# return strTimeProp(start, end, random.random(), fmt)
|
||||
#
|
||||
#
|
||||
# def randomDate(start, end, frmt='%Y-%m-%d %H:%M:%S'):
|
||||
# return time.strftime(frmt, time.localtime(strTimeProp(start, end, random.random(), frmt)))
|
||||
#
|
||||
#
|
||||
# def randomTimestampList(start, end, n, frmt='%Y-%m-%d %H:%M:%S'):
|
||||
# return [randomTimestamp(start, end, frmt) for _ in range(n)]
|
||||
#
|
||||
#
|
||||
# def randomDateList(start, end, n, frmt='%Y-%m-%d %H:%M:%S'):
|
||||
# return [randomDate(start, end, frmt) for _ in range(n)]
|
||||
|
||||
|
|
|
@ -300,32 +300,6 @@ class IndexApi(DatesLoginMetricMixin, APIView):
|
|||
class HealthApiMixin(APIView):
|
||||
pass
|
||||
|
||||
# 先去掉 Health Api 的权限校验,方便各组件直接调用
|
||||
|
||||
# def is_token_right(self):
|
||||
# token = self.request.query_params.get('token')
|
||||
# ok_token = settings.HEALTH_CHECK_TOKEN
|
||||
# if ok_token and token != ok_token:
|
||||
# return False
|
||||
# return True
|
||||
|
||||
# def is_localhost(self):
|
||||
# ip = get_request_ip(self.request)
|
||||
# return ip in ['localhost', '127.0.0.1']
|
||||
|
||||
# def check_permissions(self, request):
|
||||
# if self.is_token_right():
|
||||
# return
|
||||
# if self.is_localhost():
|
||||
# return
|
||||
# msg = '''
|
||||
# Health check token error,
|
||||
# Please set query param in url and
|
||||
# same with setting HEALTH_CHECK_TOKEN.
|
||||
# eg: $PATH/?token=$HEALTH_CHECK_TOKEN
|
||||
# '''
|
||||
# self.permission_denied(request, message={'error': msg}, code=403)
|
||||
|
||||
|
||||
class HealthCheckView(HealthApiMixin):
|
||||
permission_classes = (AllowAny,)
|
||||
|
|
|
@ -16,8 +16,10 @@ import json
|
|||
import yaml
|
||||
import copy
|
||||
from importlib import import_module
|
||||
from django.urls import reverse_lazy
|
||||
from urllib.parse import urljoin, urlparse
|
||||
|
||||
from django.urls import reverse_lazy
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
@ -174,6 +176,7 @@ class Config(dict):
|
|||
'AUTH_LDAP_SYNC_IS_PERIODIC': False,
|
||||
'AUTH_LDAP_SYNC_INTERVAL': None,
|
||||
'AUTH_LDAP_SYNC_CRONTAB': None,
|
||||
'AUTH_LDAP_SYNC_ORG_ID': '00000000-0000-0000-0000-000000000002',
|
||||
'AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS': False,
|
||||
'AUTH_LDAP_OPTIONS_OPT_REFERRALS': -1,
|
||||
|
||||
|
@ -253,6 +256,8 @@ class Config(dict):
|
|||
'AUTH_SAML2_PROVIDER_AUTHORIZATION_ENDPOINT': '/',
|
||||
'AUTH_SAML2_AUTHENTICATION_FAILURE_REDIRECT_URI': '/',
|
||||
|
||||
'AUTH_TEMP_TOKEN': False,
|
||||
|
||||
# 企业微信
|
||||
'AUTH_WECOM': False,
|
||||
'WECOM_CORPID': '',
|
||||
|
@ -307,7 +312,11 @@ class Config(dict):
|
|||
'TERMINAL_HOST_KEY': '',
|
||||
'TERMINAL_TELNET_REGEX': '',
|
||||
'TERMINAL_COMMAND_STORAGE': {},
|
||||
# 未来废弃(当下迁移会用)
|
||||
'TERMINAL_RDP_ADDR': '',
|
||||
# 保留(Luna还在用)
|
||||
'TERMINAL_MAGNUS_ENABLED': True,
|
||||
# 保留(Luna还在用)
|
||||
'XRDP_ENABLED': True,
|
||||
|
||||
# 安全配置
|
||||
|
@ -392,6 +401,7 @@ class Config(dict):
|
|||
|
||||
'FORGOT_PASSWORD_URL': '',
|
||||
'HEALTH_CHECK_TOKEN': '',
|
||||
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
|
@ -540,7 +550,8 @@ class Config(dict):
|
|||
value = self.get_from_env(item)
|
||||
if value is not None:
|
||||
return value
|
||||
return self.defaults.get(item)
|
||||
value = self.defaults.get(item)
|
||||
return value
|
||||
|
||||
def __getitem__(self, item):
|
||||
return self.get(item)
|
||||
|
|
|
@ -1,8 +1,40 @@
|
|||
from redis_sessions.session import force_unicode, SessionStore as RedisSessionStore
|
||||
from redis import exceptions
|
||||
from redis_sessions.session import (
|
||||
force_unicode, SessionStore as RedisSessionStore,
|
||||
RedisServer as _RedisServer, settings as redis_setting
|
||||
)
|
||||
from redis import exceptions, Redis
|
||||
from django.conf import settings
|
||||
|
||||
from jumpserver.const import CONFIG
|
||||
|
||||
|
||||
class RedisServer(_RedisServer):
|
||||
__redis = {}
|
||||
|
||||
def get(self):
|
||||
if self.connection_key in self.__redis:
|
||||
return self.__redis[self.connection_key]
|
||||
|
||||
ssl_params = {}
|
||||
if CONFIG.REDIS_USE_SSL:
|
||||
ssl_params = {
|
||||
'ssl_cert_reqs': CONFIG.REDIS_SSL_REQUIRED,
|
||||
'ssl_keyfile': getattr(settings, 'REDIS_SSL_KEYFILE'),
|
||||
'ssl_certfile': getattr(settings, 'REDIS_SSL_CERTFILE'),
|
||||
'ssl_ca_certs': getattr(settings, 'REDIS_SSL_CA_CERTS'),
|
||||
}
|
||||
# 只根据 redis_url 方式连接
|
||||
self.__redis[self.connection_key] = Redis.from_url(
|
||||
redis_setting.SESSION_REDIS_URL, **ssl_params
|
||||
)
|
||||
|
||||
return self.__redis[self.connection_key]
|
||||
|
||||
|
||||
class SessionStore(RedisSessionStore):
|
||||
def __init__(self, session_key=None):
|
||||
super(SessionStore, self).__init__(session_key)
|
||||
self.server = RedisServer(session_key).get()
|
||||
|
||||
def load(self):
|
||||
try:
|
||||
|
|
|
@ -43,6 +43,7 @@ AUTH_LDAP_SEARCH_PAGED_SIZE = CONFIG.AUTH_LDAP_SEARCH_PAGED_SIZE
|
|||
AUTH_LDAP_SYNC_IS_PERIODIC = CONFIG.AUTH_LDAP_SYNC_IS_PERIODIC
|
||||
AUTH_LDAP_SYNC_INTERVAL = CONFIG.AUTH_LDAP_SYNC_INTERVAL
|
||||
AUTH_LDAP_SYNC_CRONTAB = CONFIG.AUTH_LDAP_SYNC_CRONTAB
|
||||
AUTH_LDAP_SYNC_ORG_ID = CONFIG.AUTH_LDAP_SYNC_ORG_ID
|
||||
AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS = CONFIG.AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS
|
||||
|
||||
|
||||
|
@ -108,11 +109,11 @@ CAS_APPLY_ATTRIBUTES_TO_USER = CONFIG.CAS_APPLY_ATTRIBUTES_TO_USER
|
|||
CAS_RENAME_ATTRIBUTES = CONFIG.CAS_RENAME_ATTRIBUTES
|
||||
CAS_CREATE_USER = CONFIG.CAS_CREATE_USER
|
||||
|
||||
# SSO Auth
|
||||
# SSO auth
|
||||
AUTH_SSO = CONFIG.AUTH_SSO
|
||||
AUTH_SSO_AUTHKEY_TTL = CONFIG.AUTH_SSO_AUTHKEY_TTL
|
||||
|
||||
# WECOM Auth
|
||||
# WECOM auth
|
||||
AUTH_WECOM = CONFIG.AUTH_WECOM
|
||||
WECOM_CORPID = CONFIG.WECOM_CORPID
|
||||
WECOM_AGENTID = CONFIG.WECOM_AGENTID
|
||||
|
@ -140,6 +141,9 @@ SAML2_SP_ADVANCED_SETTINGS = CONFIG.SAML2_SP_ADVANCED_SETTINGS
|
|||
SAML2_LOGIN_URL_NAME = "authentication:saml2:saml2-login"
|
||||
SAML2_LOGOUT_URL_NAME = "authentication:saml2:saml2-logout"
|
||||
|
||||
# 临时 token
|
||||
AUTH_TEMP_TOKEN = CONFIG.AUTH_TEMP_TOKEN
|
||||
|
||||
# Other setting
|
||||
TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION
|
||||
OTP_IN_RADIUS = CONFIG.OTP_IN_RADIUS
|
||||
|
@ -159,6 +163,7 @@ AUTH_BACKEND_DINGTALK = 'authentication.backends.sso.DingTalkAuthentication'
|
|||
AUTH_BACKEND_FEISHU = 'authentication.backends.sso.FeiShuAuthentication'
|
||||
AUTH_BACKEND_AUTH_TOKEN = 'authentication.backends.sso.AuthorizationTokenAuthentication'
|
||||
AUTH_BACKEND_SAML2 = 'authentication.backends.saml2.SAML2Backend'
|
||||
AUTH_BACKEND_TEMP_TOKEN = 'authentication.backends.token.TempTokenAuthBackend'
|
||||
|
||||
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
|
@ -171,7 +176,7 @@ AUTHENTICATION_BACKENDS = [
|
|||
# 扫码模式
|
||||
AUTH_BACKEND_WECOM, AUTH_BACKEND_DINGTALK, AUTH_BACKEND_FEISHU,
|
||||
# Token模式
|
||||
AUTH_BACKEND_AUTH_TOKEN, AUTH_BACKEND_SSO,
|
||||
AUTH_BACKEND_AUTH_TOKEN, AUTH_BACKEND_SSO, AUTH_BACKEND_TEMP_TOKEN
|
||||
]
|
||||
|
||||
ONLY_ALLOW_EXIST_USER_AUTH = CONFIG.ONLY_ALLOW_EXIST_USER_AUTH
|
||||
|
|
|
@ -127,7 +127,7 @@ LOGIN_REDIRECT_URL = reverse_lazy('index')
|
|||
LOGIN_URL = reverse_lazy('authentication:login')
|
||||
|
||||
SESSION_COOKIE_DOMAIN = CONFIG.SESSION_COOKIE_DOMAIN
|
||||
CSRF_COOKIE_DOMAIN = CONFIG.CSRF_COOKIE_DOMAIN
|
||||
CSRF_COOKIE_DOMAIN = CONFIG.SESSION_COOKIE_DOMAIN
|
||||
SESSION_COOKIE_AGE = CONFIG.SESSION_COOKIE_AGE
|
||||
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
|
||||
# 自定义的配置,SESSION_EXPIRE_AT_BROWSER_CLOSE 始终为 True, 下面这个来控制是否强制关闭后过期 cookie
|
||||
|
@ -135,10 +135,13 @@ SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE = CONFIG.SESSION_EXPIRE_AT_BROWSER_CLOSE_F
|
|||
SESSION_SAVE_EVERY_REQUEST = CONFIG.SESSION_SAVE_EVERY_REQUEST
|
||||
SESSION_ENGINE = 'jumpserver.rewriting.session'
|
||||
SESSION_REDIS = {
|
||||
'url': '%(protocol)s://:%(password)s@%(host)s:%(port)s/%(db)s' % {
|
||||
'protocol': 'rediss' if CONFIG.REDIS_USE_SSL else 'redis',
|
||||
'password': CONFIG.REDIS_PASSWORD,
|
||||
'host': CONFIG.REDIS_HOST,
|
||||
'port': CONFIG.REDIS_PORT,
|
||||
'password': CONFIG.REDIS_PASSWORD,
|
||||
'db': CONFIG.REDIS_DB_SESSION,
|
||||
'db': CONFIG.REDIS_DB_CACHE,
|
||||
},
|
||||
'prefix': 'auth_session',
|
||||
'socket_timeout': 1,
|
||||
'retry_on_timeout': False
|
||||
|
@ -246,18 +249,37 @@ FILE_UPLOAD_PERMISSIONS = 0o644
|
|||
FILE_UPLOAD_DIRECTORY_PERMISSIONS = 0o755
|
||||
|
||||
# Cache use redis
|
||||
REDIS_SSL_KEYFILE = os.path.join(PROJECT_DIR, 'data', 'certs', 'redis_client.key')
|
||||
if not os.path.exists(REDIS_SSL_KEYFILE):
|
||||
REDIS_SSL_KEYFILE = None
|
||||
|
||||
REDIS_SSL_CERTFILE = os.path.join(PROJECT_DIR, 'data', 'certs', 'redis_client.crt')
|
||||
if not os.path.exists(REDIS_SSL_CERTFILE):
|
||||
REDIS_SSL_CERTFILE = None
|
||||
|
||||
REDIS_SSL_CA_CERTS = os.path.join(PROJECT_DIR, 'data', 'certs', 'redis_ca.crt')
|
||||
if not os.path.exists(REDIS_SSL_CA_CERTS):
|
||||
REDIS_SSL_CA_CERTS = os.path.join(PROJECT_DIR, 'data', 'certs', 'redis_ca.pem')
|
||||
|
||||
CACHES = {
|
||||
'default': {
|
||||
# 'BACKEND': 'redis_cache.RedisCache',
|
||||
'BACKEND': 'redis_lock.django_cache.RedisCache',
|
||||
'LOCATION': 'redis://:%(password)s@%(host)s:%(port)s/%(db)s' % {
|
||||
'LOCATION': '%(protocol)s://:%(password)s@%(host)s:%(port)s/%(db)s' % {
|
||||
'protocol': 'rediss' if CONFIG.REDIS_USE_SSL else 'redis',
|
||||
'password': CONFIG.REDIS_PASSWORD,
|
||||
'host': CONFIG.REDIS_HOST,
|
||||
'port': CONFIG.REDIS_PORT,
|
||||
'db': CONFIG.REDIS_DB_CACHE,
|
||||
},
|
||||
'OPTIONS': {
|
||||
"REDIS_CLIENT_KWARGS": {"health_check_interval": 30}
|
||||
"REDIS_CLIENT_KWARGS": {"health_check_interval": 30},
|
||||
"CONNECTION_POOL_KWARGS": {
|
||||
'ssl_cert_reqs': CONFIG.REDIS_SSL_REQUIRED,
|
||||
"ssl_keyfile": REDIS_SSL_KEYFILE,
|
||||
"ssl_certfile": REDIS_SSL_CERTFILE,
|
||||
"ssl_ca_certs": REDIS_SSL_CA_CERTS
|
||||
} if CONFIG.REDIS_USE_SSL else {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import os
|
||||
import ssl
|
||||
|
||||
from .base import REDIS_SSL_CA_CERTS, REDIS_SSL_CERTFILE, REDIS_SSL_KEYFILE
|
||||
from ..const import CONFIG, PROJECT_DIR
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
|
@ -82,16 +85,25 @@ BOOTSTRAP3 = {
|
|||
|
||||
|
||||
# Django channels support websocket
|
||||
CHANNEL_REDIS = "redis://:{}@{}:{}/{}".format(
|
||||
CONFIG.REDIS_PASSWORD, CONFIG.REDIS_HOST, CONFIG.REDIS_PORT,
|
||||
CONFIG.REDIS_DB_WS,
|
||||
)
|
||||
if not CONFIG.REDIS_USE_SSL:
|
||||
context = None
|
||||
else:
|
||||
context = ssl.SSLContext()
|
||||
context.check_hostname = bool(CONFIG.REDIS_SSL_REQUIRED)
|
||||
context.load_verify_locations(REDIS_SSL_CA_CERTS)
|
||||
if REDIS_SSL_CERTFILE and REDIS_SSL_KEYFILE:
|
||||
context.load_cert_chain(REDIS_SSL_CERTFILE, REDIS_SSL_KEYFILE)
|
||||
|
||||
CHANNEL_LAYERS = {
|
||||
'default': {
|
||||
'BACKEND': 'channels_redis.core.RedisChannelLayer',
|
||||
'CONFIG': {
|
||||
"hosts": [CHANNEL_REDIS],
|
||||
"hosts": [{
|
||||
'address': (CONFIG.REDIS_HOST, CONFIG.REDIS_PORT),
|
||||
'db': CONFIG.REDIS_DB_WS,
|
||||
'password': CONFIG.REDIS_PASSWORD,
|
||||
'ssl': context
|
||||
}],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -102,7 +114,8 @@ ASGI_APPLICATION = 'jumpserver.routing.application'
|
|||
CELERY_LOG_DIR = os.path.join(PROJECT_DIR, 'data', 'celery')
|
||||
|
||||
# Celery using redis as broker
|
||||
CELERY_BROKER_URL = 'redis://:%(password)s@%(host)s:%(port)s/%(db)s' % {
|
||||
CELERY_BROKER_URL = '%(protocol)s://:%(password)s@%(host)s:%(port)s/%(db)s' % {
|
||||
'protocol': 'rediss' if CONFIG.REDIS_USE_SSL else 'redis',
|
||||
'password': CONFIG.REDIS_PASSWORD,
|
||||
'host': CONFIG.REDIS_HOST,
|
||||
'port': CONFIG.REDIS_PORT,
|
||||
|
@ -125,6 +138,13 @@ CELERY_WORKER_REDIRECT_STDOUTS_LEVEL = "INFO"
|
|||
# CELERY_WORKER_HIJACK_ROOT_LOGGER = True
|
||||
# CELERY_WORKER_MAX_TASKS_PER_CHILD = 40
|
||||
CELERY_TASK_SOFT_TIME_LIMIT = 3600
|
||||
if CONFIG.REDIS_USE_SSL:
|
||||
CELERY_BROKER_USE_SSL = CELERY_REDIS_BACKEND_USE_SSL = {
|
||||
'ssl_cert_reqs': CONFIG.REDIS_SSL_REQUIRED,
|
||||
'ssl_ca_certs': REDIS_SSL_CA_CERTS,
|
||||
'ssl_certfile': REDIS_SSL_CERTFILE,
|
||||
'ssl_keyfile': REDIS_SSL_KEYFILE
|
||||
}
|
||||
|
||||
ANSIBLE_LOG_DIR = os.path.join(PROJECT_DIR, 'data', 'ansible')
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from django.views.generic import TemplateView
|
||||
from django.views.generic import View
|
||||
from django.shortcuts import redirect
|
||||
from common.permissions import IsValidUser
|
||||
from common.mixins.views import PermissionsMixin
|
||||
|
@ -6,8 +6,7 @@ from common.mixins.views import PermissionsMixin
|
|||
__all__ = ['IndexView']
|
||||
|
||||
|
||||
class IndexView(PermissionsMixin, TemplateView):
|
||||
template_name = 'index.html'
|
||||
class IndexView(PermissionsMixin, View):
|
||||
permission_classes = [IsValidUser]
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:89878c511a62211520b347ccf37676cb11e9a0b3257ff968fb6d5dd81726a1e5
|
||||
size 125117
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1c4a4fa3abb21fea213011d50fd62455fb1ddf73538401dc8cd9c03f4f4bbc77
|
||||
size 3322
|
|
@ -0,0 +1,158 @@
|
|||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-03-22 15:29+0800\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
|
||||
#: static/js/jumpserver.js:260
|
||||
msgid "Update is successful!"
|
||||
msgstr "アップデートは成功しました!"
|
||||
|
||||
#: static/js/jumpserver.js:262
|
||||
msgid "An unknown error occurred while updating.."
|
||||
msgstr "更新中に不明なエラーが発生しました。"
|
||||
|
||||
#: static/js/jumpserver.js:333
|
||||
msgid "Not found"
|
||||
msgstr "見つかりません"
|
||||
|
||||
#: static/js/jumpserver.js:335
|
||||
msgid "Server error"
|
||||
msgstr "サーバーエラー"
|
||||
|
||||
#: static/js/jumpserver.js:337 static/js/jumpserver.js:375
|
||||
#: static/js/jumpserver.js:377
|
||||
msgid "Error"
|
||||
msgstr "エラー"
|
||||
|
||||
#: static/js/jumpserver.js:343 static/js/jumpserver.js:384
|
||||
msgid "Delete the success"
|
||||
msgstr "成功を削除する"
|
||||
|
||||
#: static/js/jumpserver.js:350
|
||||
msgid "Are you sure about deleting it?"
|
||||
msgstr "削除してもよろしいですか?"
|
||||
|
||||
#: static/js/jumpserver.js:354 static/js/jumpserver.js:395
|
||||
msgid "Cancel"
|
||||
msgstr "キャンセル"
|
||||
|
||||
#: static/js/jumpserver.js:356 static/js/jumpserver.js:397
|
||||
msgid "Confirm"
|
||||
msgstr "確認"
|
||||
|
||||
#: static/js/jumpserver.js:375
|
||||
msgid ""
|
||||
"The organization contains undeleted information. Please try again after "
|
||||
"deleting"
|
||||
msgstr "組織には削除されていない情報が含まれています。削除後にもう一度お試しください"
|
||||
|
||||
#: static/js/jumpserver.js:377
|
||||
msgid ""
|
||||
"Do not perform this operation under this organization. Try again after "
|
||||
"switching to another organization"
|
||||
msgstr "この組織ではこの操作を実行しないでください。別の組織に切り替えた後にもう一度お試しください"
|
||||
|
||||
#: static/js/jumpserver.js:391
|
||||
msgid ""
|
||||
"Please ensure that the following information in the organization has been "
|
||||
"deleted"
|
||||
msgstr "組織内の次の情報が削除されていることを確認してください"
|
||||
|
||||
#: static/js/jumpserver.js:392
|
||||
msgid ""
|
||||
"User list、User group、Asset list、Domain list、Admin user、System user、"
|
||||
"Labels、Asset permission"
|
||||
msgstr "ユーザーリスト、ユーザーグループ、資産リスト、ドメインリスト、管理ユーザー、システムユーザー、ラベル、資産権限"
|
||||
|
||||
#: static/js/jumpserver.js:469
|
||||
msgid "Loading"
|
||||
msgstr "読み込み中"
|
||||
|
||||
#: static/js/jumpserver.js:470
|
||||
msgid "Search"
|
||||
msgstr "検索"
|
||||
|
||||
#: static/js/jumpserver.js:473
|
||||
#, javascript-format
|
||||
msgid "Selected item %d"
|
||||
msgstr "選択したアイテム % d"
|
||||
|
||||
#: static/js/jumpserver.js:477
|
||||
msgid "Per page _MENU_"
|
||||
msgstr "各ページ _MENU_"
|
||||
|
||||
#: static/js/jumpserver.js:478
|
||||
msgid ""
|
||||
"Displays the results of items _START_ to _END_; A total of _TOTAL_ entries"
|
||||
msgstr "アイテムの結果を表示します _START_ に着く _END_; 合計 _TOTAL_ エントリ"
|
||||
|
||||
#: static/js/jumpserver.js:481
|
||||
msgid "No match"
|
||||
msgstr "一致しません"
|
||||
|
||||
#: static/js/jumpserver.js:482
|
||||
msgid "No record"
|
||||
msgstr "記録なし"
|
||||
|
||||
#: static/js/jumpserver.js:662
|
||||
msgid "Unknown error occur"
|
||||
msgstr "不明なエラーが発生"
|
||||
|
||||
#: static/js/jumpserver.js:915
|
||||
msgid "Password minimum length {N} bits"
|
||||
msgstr "最小パスワード長 {N} ビット"
|
||||
|
||||
#: static/js/jumpserver.js:916
|
||||
msgid "Must contain capital letters"
|
||||
msgstr "大文字を含める必要があります"
|
||||
|
||||
#: static/js/jumpserver.js:917
|
||||
msgid "Must contain lowercase letters"
|
||||
msgstr "小文字を含める必要があります"
|
||||
|
||||
#: static/js/jumpserver.js:918
|
||||
msgid "Must contain numeric characters"
|
||||
msgstr "数字を含める必要があります。"
|
||||
|
||||
#: static/js/jumpserver.js:919
|
||||
msgid "Must contain special characters"
|
||||
msgstr "特殊文字を含める必要があります"
|
||||
|
||||
#: static/js/jumpserver.js:1098 static/js/jumpserver.js:1122
|
||||
msgid "Export failed"
|
||||
msgstr "エクスポートに失敗しました"
|
||||
|
||||
#: static/js/jumpserver.js:1139
|
||||
msgid "Import Success"
|
||||
msgstr "インポートの成功"
|
||||
|
||||
#: static/js/jumpserver.js:1144
|
||||
msgid "Update Success"
|
||||
msgstr "更新の成功"
|
||||
|
||||
#: static/js/jumpserver.js:1145
|
||||
msgid "Count"
|
||||
msgstr "カウント"
|
||||
|
||||
#: static/js/jumpserver.js:1174
|
||||
msgid "Import failed"
|
||||
msgstr "インポートに失敗しました"
|
||||
|
||||
#: static/js/jumpserver.js:1179
|
||||
msgid "Update failed"
|
||||
msgstr "更新に失敗しました"
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:449810c3661c09f6448b9c67e7a193f303a3bef7ccc3d0f1efe6e099804e782a
|
||||
size 104323
|
||||
oid sha256:c5e41035cf1525f01fb773511041f0f8a3a25cdfb1fa4f1e681c6d7eec85f6b9
|
||||
size 103570
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -59,7 +59,7 @@ class CommandExecutionViewSet(RootOrgViewMixin, viewsets.ModelViewSet):
|
|||
raise ValidationError({"hosts": msg})
|
||||
|
||||
def check_permissions(self, request):
|
||||
if not settings.SECURITY_COMMAND_EXECUTION and request.user.is_common_user:
|
||||
if not settings.SECURITY_COMMAND_EXECUTION:
|
||||
return self.permission_denied(request, "Command execution disabled")
|
||||
return super().check_permissions(request)
|
||||
|
||||
|
|
|
@ -29,13 +29,7 @@ class JMSBaseInventory(BaseInventory):
|
|||
if asset.domain and asset.domain.has_gateway():
|
||||
info["vars"].update(self.make_proxy_command(asset))
|
||||
if run_as_admin:
|
||||
info.update(asset.get_auth_info())
|
||||
if asset.is_unixlike():
|
||||
info["become"] = {
|
||||
"method": 'sudo',
|
||||
"user": 'root',
|
||||
"pass": ''
|
||||
}
|
||||
info.update(asset.get_auth_info(with_become=True))
|
||||
if asset.is_windows():
|
||||
info["vars"].update({
|
||||
"ansible_connection": "ssh",
|
||||
|
|
|
@ -1,33 +1,61 @@
|
|||
from functools import wraps
|
||||
from django.db.models.signals import post_save, pre_delete, pre_save, post_delete
|
||||
from django.dispatch import receiver
|
||||
|
||||
from orgs.models import Organization
|
||||
from assets.models import Node
|
||||
from perms.models import (AssetPermission, ApplicationPermission)
|
||||
from perms.models import AssetPermission, ApplicationPermission
|
||||
from users.models import UserGroup, User
|
||||
from users.signals import pre_user_leave_org
|
||||
from applications.models import Application
|
||||
from terminal.models import Session
|
||||
from rbac.models import OrgRoleBinding, SystemRoleBinding
|
||||
from assets.models import Asset, SystemUser, Domain, Gateway
|
||||
from orgs.caches import OrgResourceStatisticsCache
|
||||
from orgs.utils import current_org
|
||||
from common.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def refresh_user_amount_on_user_create_or_delete(user_id):
|
||||
orgs = Organization.objects.filter(m2m_org_members__user_id=user_id).distinct()
|
||||
def refresh_cache(name, org):
|
||||
names = None
|
||||
if isinstance(name, (str,)):
|
||||
names = [name, ]
|
||||
if isinstance(names, (list, tuple)):
|
||||
for name in names:
|
||||
OrgResourceStatisticsCache(org).expire(name)
|
||||
OrgResourceStatisticsCache(Organization.root()).expire(name)
|
||||
else:
|
||||
logger.warning('refresh cache fail: {}'.format(name))
|
||||
|
||||
|
||||
def refresh_user_amount_cache(user):
|
||||
orgs = user.orgs.distinct()
|
||||
for org in orgs:
|
||||
org_cache = OrgResourceStatisticsCache(org)
|
||||
org_cache.expire('users_amount')
|
||||
OrgResourceStatisticsCache(Organization.root()).expire('users_amount')
|
||||
refresh_cache('users_amount', org)
|
||||
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def on_user_create_refresh_cache(sender, instance, created, **kwargs):
|
||||
@receiver(post_save, sender=OrgRoleBinding)
|
||||
def on_user_create_or_invite_refresh_cache(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
refresh_user_amount_on_user_create_or_delete(instance.id)
|
||||
refresh_cache('users_amount', instance.org)
|
||||
|
||||
|
||||
@receiver(post_save, sender=SystemRoleBinding)
|
||||
def on_user_global_create_refresh_cache(sender, instance, created, **kwargs):
|
||||
if created and current_org.is_root():
|
||||
refresh_cache('users_amount', current_org)
|
||||
|
||||
|
||||
@receiver(pre_user_leave_org)
|
||||
def on_user_remove_refresh_cache(sender, org=None, **kwargs):
|
||||
refresh_cache('users_amount', org)
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=User)
|
||||
def on_user_delete_refresh_cache(sender, instance, **kwargs):
|
||||
refresh_user_amount_on_user_create_or_delete(instance.id)
|
||||
refresh_user_amount_cache(instance)
|
||||
|
||||
|
||||
# @receiver(m2m_changed, sender=OrganizationMember)
|
||||
|
|
|
@ -22,7 +22,3 @@ class AppRoleUserMixin(_RoleUserMixin):
|
|||
('get_tree', 'perms.view_myapps'),
|
||||
('GET', 'perms.view_myapps'),
|
||||
)
|
||||
|
||||
def dispatch(self, *args, **kwargs):
|
||||
with tmp_to_root_org():
|
||||
return super().dispatch(*args, **kwargs)
|
|
@ -1,18 +1,18 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from typing import Callable
|
||||
|
||||
from rest_framework.generics import ListAPIView
|
||||
from rest_framework.response import Response
|
||||
|
||||
from common.mixins.api import CommonApiMixin
|
||||
from common.tree import TreeNodeSerializer
|
||||
from applications.api.mixin import (
|
||||
SerializeApplicationToTreeNodeMixin
|
||||
)
|
||||
from perms import serializers
|
||||
from .mixin import AppRoleAdminMixin, AppRoleUserMixin
|
||||
from perms.tree.app import GrantedAppTreeUtil
|
||||
from perms.utils.application.user_permission import (
|
||||
get_user_granted_all_applications
|
||||
)
|
||||
from .mixin import AppRoleAdminMixin, AppRoleUserMixin
|
||||
|
||||
|
||||
__all__ = [
|
||||
|
@ -23,7 +23,7 @@ __all__ = [
|
|||
]
|
||||
|
||||
|
||||
class AllGrantedApplicationsMixin(CommonApiMixin, ListAPIView):
|
||||
class AllGrantedApplicationsApi(CommonApiMixin, ListAPIView):
|
||||
only_fields = serializers.AppGrantedSerializer.Meta.only_fields
|
||||
serializer_class = serializers.AppGrantedSerializer
|
||||
filterset_fields = {
|
||||
|
@ -41,28 +41,34 @@ class AllGrantedApplicationsMixin(CommonApiMixin, ListAPIView):
|
|||
return queryset.only(*self.only_fields)
|
||||
|
||||
|
||||
class UserAllGrantedApplicationsApi(AppRoleAdminMixin, AllGrantedApplicationsMixin):
|
||||
class UserAllGrantedApplicationsApi(AppRoleAdminMixin, AllGrantedApplicationsApi):
|
||||
pass
|
||||
|
||||
|
||||
class MyAllGrantedApplicationsApi(AppRoleUserMixin, AllGrantedApplicationsMixin):
|
||||
class MyAllGrantedApplicationsApi(AppRoleUserMixin, AllGrantedApplicationsApi):
|
||||
pass
|
||||
|
||||
|
||||
class ApplicationsAsTreeMixin(SerializeApplicationToTreeNodeMixin):
|
||||
class ApplicationsAsTreeMixin:
|
||||
"""
|
||||
将应用序列化成树的结构返回
|
||||
"""
|
||||
serializer_class = TreeNodeSerializer
|
||||
user: None
|
||||
filter_queryset: Callable
|
||||
get_queryset: Callable
|
||||
get_serializer: Callable
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
tree_id = request.query_params.get('tree_id', None)
|
||||
parent_info = request.query_params.get('parentInfo', None)
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
tree_nodes = self.serialize_applications_with_org(
|
||||
queryset, tree_id, parent_info, self.user
|
||||
)
|
||||
util = GrantedAppTreeUtil()
|
||||
|
||||
if not tree_id:
|
||||
tree_nodes = util.create_tree_nodes(queryset)
|
||||
else:
|
||||
tree_nodes = util.get_children_nodes(tree_id, parent_info, self.user)
|
||||
serializer = self.get_serializer(tree_nodes, many=True)
|
||||
return Response(data=serializer.data)
|
||||
|
||||
|
|
|
@ -36,7 +36,3 @@ class AssetRoleUserMixin(PermBaseMixin, _RoleUserMixin):
|
|||
('get_tree', 'perms.view_myassets'),
|
||||
('GET', 'perms.view_myassets'),
|
||||
)
|
||||
|
||||
def dispatch(self, *args, **kwargs):
|
||||
with tmp_to_root_org():
|
||||
return super().dispatch(*args, **kwargs)
|
||||
|
|
|
@ -6,16 +6,12 @@ from rest_framework.generics import get_object_or_404
|
|||
from common.tree import TreeNode
|
||||
from orgs.models import Organization
|
||||
from assets.models import SystemUser
|
||||
from applications.utils import KubernetesClient, KubernetesTree
|
||||
from applications.utils import KubernetesTree
|
||||
from applications.models import Application
|
||||
from perms.utils.application.permission import get_application_system_user_ids
|
||||
|
||||
from ..models import Application
|
||||
|
||||
__all__ = ['SerializeApplicationToTreeNodeMixin']
|
||||
|
||||
|
||||
class SerializeApplicationToTreeNodeMixin:
|
||||
|
||||
class GrantedAppTreeUtil:
|
||||
@staticmethod
|
||||
def filter_organizations(applications):
|
||||
organization_ids = set(applications.values_list('org_id', flat=True))
|
||||
|
@ -39,37 +35,15 @@ class SerializeApplicationToTreeNodeMixin:
|
|||
})
|
||||
return node
|
||||
|
||||
def serialize_applications_with_org(self, applications, tree_id, parent_info, user):
|
||||
@staticmethod
|
||||
def get_children_nodes(tree_id, parent_info, user):
|
||||
tree_nodes = []
|
||||
if not applications:
|
||||
return tree_nodes
|
||||
|
||||
if not tree_id:
|
||||
root_node = self.create_root_node()
|
||||
tree_nodes.append(root_node)
|
||||
organizations = self.filter_organizations(applications)
|
||||
for i, org in enumerate(organizations):
|
||||
tree_id = urlencode({'org_id': str(org.id)})
|
||||
apps = applications.filter(org_id=org.id)
|
||||
# 组织节点
|
||||
org_node = org.as_tree_node(oid=tree_id, pid=root_node.id)
|
||||
org_node.name += '({})'.format(apps.count())
|
||||
tree_nodes.append(org_node)
|
||||
category_type_nodes = Application.create_category_type_tree_nodes(
|
||||
apps, tree_id, show_empty=False
|
||||
)
|
||||
tree_nodes += category_type_nodes
|
||||
|
||||
for app in apps:
|
||||
app_node = app.as_tree_node(tree_id, is_luna=True)
|
||||
tree_nodes.append(app_node)
|
||||
return tree_nodes
|
||||
|
||||
parent_info = dict(parse_qsl(parent_info))
|
||||
pod_name = parent_info.get('pod')
|
||||
app_id = parent_info.get('app_id')
|
||||
namespace = parent_info.get('namespace')
|
||||
system_user_id = parent_info.get('system_user_id')
|
||||
|
||||
if app_id and not any([pod_name, namespace, system_user_id]):
|
||||
app = get_object_or_404(Application, id=app_id)
|
||||
system_user_ids = get_application_system_user_ids(user, app)
|
||||
|
@ -80,6 +54,34 @@ class SerializeApplicationToTreeNodeMixin:
|
|||
)
|
||||
tree_nodes.append(system_user_node)
|
||||
return tree_nodes
|
||||
|
||||
tree_nodes = KubernetesTree(tree_id).async_tree_node(parent_info)
|
||||
return tree_nodes
|
||||
|
||||
def create_tree_nodes(self, applications):
|
||||
tree_nodes = []
|
||||
if not applications:
|
||||
return tree_nodes
|
||||
|
||||
root_node = self.create_root_node()
|
||||
tree_nodes.append(root_node)
|
||||
organizations = self.filter_organizations(applications)
|
||||
|
||||
for i, org in enumerate(organizations):
|
||||
tree_id = urlencode({'org_id': str(org.id)})
|
||||
apps = applications.filter(org_id=org.id)
|
||||
|
||||
# 组织节点
|
||||
org_node = org.as_tree_node(oid=tree_id, pid=root_node.id)
|
||||
org_node.name += '({})'.format(apps.count())
|
||||
tree_nodes.append(org_node)
|
||||
|
||||
# 类别节点
|
||||
category_type_nodes = Application.create_category_type_tree_nodes(
|
||||
apps, tree_id, show_empty=False
|
||||
)
|
||||
tree_nodes += category_type_nodes
|
||||
|
||||
for app in apps:
|
||||
app_node = app.as_tree_node(tree_id, k8s_as_tree=True)
|
||||
tree_nodes.append(app_node)
|
||||
return tree_nodes
|
|
@ -202,7 +202,9 @@ class UserGrantedTreeRefreshController:
|
|||
user = self.user
|
||||
|
||||
with tmp_to_root_org():
|
||||
UserAssetGrantedTreeNodeRelation.objects.filter(user=user).exclude(org_id__in=self.org_ids).delete()
|
||||
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):
|
||||
|
@ -219,7 +221,9 @@ class UserGrantedTreeRefreshController:
|
|||
utils = UserGrantedTreeBuildUtils(user)
|
||||
utils.rebuild_user_granted_tree()
|
||||
logger.info(
|
||||
f'Rebuild user tree ok: cost={time.time() - t_start} user={self.user} org={current_org}')
|
||||
f'Rebuild user tree ok: cost={time.time() - t_start} '
|
||||
f'user={self.user} org={current_org}'
|
||||
)
|
||||
|
||||
|
||||
class UserGrantedUtilsBase:
|
||||
|
|
|
@ -16,7 +16,7 @@ class RBACBackend(JMSBaseAuthBackend):
|
|||
return False
|
||||
|
||||
def has_perm(self, user_obj, perm, obj=None):
|
||||
if not user_obj.is_active:
|
||||
if not user_obj.is_active or not perm:
|
||||
raise PermissionDenied()
|
||||
if perm == '*':
|
||||
return True
|
||||
|
|
|
@ -5,7 +5,7 @@ from .const import Scope, system_exclude_permissions, org_exclude_permissions
|
|||
# Todo: 获取应该区分 系统用户,和组织用户的权限
|
||||
# 工作台也区分组织后再考虑
|
||||
user_perms = (
|
||||
('rbac', 'menupermission', 'view', 'workspace'),
|
||||
('rbac', 'menupermission', 'view', 'workbench'),
|
||||
('rbac', 'menupermission', 'view', 'webterminal'),
|
||||
('rbac', 'menupermission', 'view', 'filemanager'),
|
||||
('perms', 'permedasset', 'view,connect', 'myassets'),
|
||||
|
@ -16,7 +16,9 @@ user_perms = (
|
|||
('applications', 'application', 'match', 'application'),
|
||||
('ops', 'commandexecution', 'add', 'commandexecution'),
|
||||
('authentication', 'connectiontoken', 'add', 'connectiontoken'),
|
||||
('authentication', 'temptoken', 'add', 'temptoken'),
|
||||
('tickets', 'ticket', 'view', 'ticket'),
|
||||
('orgs', 'organization', 'view', 'rootorg'),
|
||||
)
|
||||
|
||||
auditor_perms = user_perms + (
|
||||
|
@ -29,7 +31,6 @@ auditor_perms = user_perms + (
|
|||
('ops', 'commandexecution', 'view', 'commandexecution')
|
||||
)
|
||||
|
||||
|
||||
app_exclude_perms = [
|
||||
('users', 'user', 'add,delete', 'user'),
|
||||
('orgs', 'org', 'add,delete,change', 'org'),
|
||||
|
@ -59,7 +60,8 @@ class PredefineRole:
|
|||
from rbac.models import Role
|
||||
return Role.objects.get(id=self.id)
|
||||
|
||||
def _get_defaults(self):
|
||||
@property
|
||||
def default_perms(self):
|
||||
from rbac.models import Permission
|
||||
q = Permission.get_define_permissions_q(self.perms)
|
||||
permissions = Permission.get_permissions(self.scope)
|
||||
|
@ -72,9 +74,13 @@ class PredefineRole:
|
|||
permissions = permissions.exclude(q)
|
||||
|
||||
perms = permissions.values_list('id', flat=True)
|
||||
return perms
|
||||
|
||||
def _get_defaults(self):
|
||||
perms = self.default_perms
|
||||
defaults = {
|
||||
'id': self.id, 'name': self.name, 'scope': self.scope,
|
||||
'builtin': True, 'permissions': perms
|
||||
'builtin': True, 'permissions': perms, 'created_by': 'System',
|
||||
}
|
||||
return defaults
|
||||
|
||||
|
|
|
@ -22,7 +22,6 @@ exclude_permissions = (
|
|||
('common', 'setting', '*', '*'),
|
||||
|
||||
('authentication', 'privatetoken', '*', '*'),
|
||||
('authentication', 'accesskey', 'change,delete', 'accesskey'),
|
||||
('authentication', 'connectiontoken', 'change,delete', 'connectiontoken'),
|
||||
('authentication', 'ssotoken', '*', '*'),
|
||||
('authentication', 'superconnectiontoken', 'change,delete', 'superconnectiontoken'),
|
||||
|
@ -49,6 +48,8 @@ exclude_permissions = (
|
|||
('rbac', 'contenttype', '*', '*'),
|
||||
('rbac', 'permission', 'add,delete,change', 'permission'),
|
||||
('rbac', 'rolebinding', '*', '*'),
|
||||
('rbac', 'systemrolebinding', 'change', 'systemrolebinding'),
|
||||
('rbac', 'orgrolebinding', 'change', 'orgrolebinding'),
|
||||
('rbac', 'role', '*', '*'),
|
||||
('ops', 'adhoc', 'delete,change', '*'),
|
||||
('ops', 'adhocexecution', 'add,delete,change', '*'),
|
||||
|
@ -99,6 +100,7 @@ only_system_permissions = (
|
|||
('orgs', 'organization', '*', '*'),
|
||||
('xpack', 'license', '*', '*'),
|
||||
('settings', 'setting', '*', '*'),
|
||||
('tickets', '*', '*', '*'),
|
||||
('ops', 'task', 'view', 'taskmonitor'),
|
||||
('terminal', 'terminal', '*', '*'),
|
||||
('terminal', 'commandstorage', '*', '*'),
|
||||
|
@ -106,6 +108,7 @@ only_system_permissions = (
|
|||
('terminal', 'status', '*', '*'),
|
||||
('terminal', 'task', '*', '*'),
|
||||
('authentication', '*', '*', '*'),
|
||||
('tickets', '*', '*', '*'),
|
||||
)
|
||||
|
||||
only_org_permissions = (
|
||||
|
|
|
@ -27,7 +27,7 @@ class Migration(migrations.Migration):
|
|||
],
|
||||
options={
|
||||
'verbose_name': 'Menu permission',
|
||||
'permissions': [('view_console', 'Can view console view'), ('view_audit', 'Can view audit view'), ('view_workspace', 'Can view workspace view')],
|
||||
'permissions': [('view_console', 'Can view console view'), ('view_audit', 'Can view audit view'), ('view_workspace', 'Can view workbench view')],
|
||||
'default_permissions': [],
|
||||
},
|
||||
),
|
||||
|
|
|
@ -12,6 +12,6 @@ class Migration(migrations.Migration):
|
|||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='menupermission',
|
||||
options={'default_permissions': [], 'permissions': [('view_console', 'Can view console view'), ('view_audit', 'Can view audit view'), ('view_workspace', 'Can view workspace view'), ('view_webterminal', 'Can view web terminal'), ('view_filemanager', 'Can view file manager')], 'verbose_name': 'Menu permission'},
|
||||
options={'default_permissions': [], 'permissions': [('view_console', 'Can view console view'), ('view_audit', 'Can view audit view'), ('view_workspace', 'Can view workbench view'), ('view_webterminal', 'Can view web terminal'), ('view_filemanager', 'Can view file manager')], 'verbose_name': 'Menu permission'},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -12,6 +12,6 @@ class Migration(migrations.Migration):
|
|||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='menupermission',
|
||||
options={'default_permissions': [], 'permissions': [('view_console', 'Can view console view'), ('view_audit', 'Can view audit view'), ('view_workspace', 'Can view workspace view'), ('view_webterminal', 'Can view web terminal'), ('view_filemanager', 'Can view file manager') ], 'verbose_name': 'Menu permission'},
|
||||
options={'default_permissions': [], 'permissions': [('view_console', 'Can view console view'), ('view_audit', 'Can view audit view'), ('view_workspace', 'Can view workbench view'), ('view_webterminal', 'Can view web terminal'), ('view_filemanager', 'Can view file manager') ], 'verbose_name': 'Menu permission'},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
# Generated by Django 3.1.14 on 2022-04-11 09:09
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def migrate_workspace_to_workbench(apps, *args):
|
||||
model = apps.get_model('auth', 'Permission')
|
||||
model.objects.filter(codename='view_workspace').delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('rbac', '0007_auto_20220314_1525'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='menupermission',
|
||||
options={'default_permissions': [], 'permissions': [('view_console', 'Can view console view'), ('view_audit', 'Can view audit view'), ('view_workbench', 'Can view workbench view'), ('view_webterminal', 'Can view web terminal'), ('view_filemanager', 'Can view file manager')], 'verbose_name': 'Menu permission'},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 3.1.14 on 2022-04-11 09:24
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def migrate_workspace_to_workbench(apps, *args):
|
||||
model = apps.get_model('auth', 'Permission')
|
||||
model.objects.filter(codename='view_workspace').delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('rbac', '0008_auto_20220411_1709'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(migrate_workspace_to_workbench)
|
||||
]
|
|
@ -14,7 +14,7 @@ class MenuPermission(models.Model):
|
|||
permissions = [
|
||||
('view_console', _('Can view console view')),
|
||||
('view_audit', _('Can view audit view')),
|
||||
('view_workspace', _('Can view workspace view')),
|
||||
('view_workbench', _('Can view workbench view')),
|
||||
('view_webterminal', _('Can view web terminal')),
|
||||
('view_filemanager', _('Can view file manager')),
|
||||
]
|
||||
|
|
|
@ -118,6 +118,9 @@ class Role(JMSModel):
|
|||
return self.name
|
||||
return gettext(self.name)
|
||||
|
||||
def is_org(self):
|
||||
return self.scope == const.Scope.org
|
||||
|
||||
|
||||
class SystemRole(Role):
|
||||
objects = SystemRoleManager()
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.core.exceptions import ValidationError
|
||||
from rest_framework.serializers import ValidationError
|
||||
|
||||
from common.db.models import JMSModel
|
||||
|
@ -67,6 +68,7 @@ class RoleBinding(JMSModel):
|
|||
|
||||
def save(self, *args, **kwargs):
|
||||
self.scope = self.role.scope
|
||||
self.clean()
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
|
@ -95,6 +97,9 @@ class RoleBinding(JMSModel):
|
|||
def role_display(self):
|
||||
return self.role.display_name
|
||||
|
||||
def is_scope_org(self):
|
||||
return self.scope == Scope.org
|
||||
|
||||
|
||||
class OrgRoleBindingManager(RoleBindingManager):
|
||||
def get_queryset(self):
|
||||
|
@ -147,3 +152,12 @@ class SystemRoleBinding(RoleBinding):
|
|||
def save(self, *args, **kwargs):
|
||||
self.scope = Scope.system
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
kwargs = dict(role=self.role, user=self.user, scope=self.scope)
|
||||
exists = self.__class__.objects.filter(**kwargs).exists()
|
||||
if exists:
|
||||
msg = "Duplicate for key 'role_user' of system role binding, {}_{}".format(
|
||||
self.role.id, self.user.id
|
||||
)
|
||||
raise ValidationError(msg)
|
||||
|
|
|
@ -93,7 +93,7 @@ class RBACPermission(permissions.DjangoModelPermissions):
|
|||
try:
|
||||
queryset = self._queryset(view)
|
||||
model_cls = queryset.model
|
||||
except AssertionError:
|
||||
except:
|
||||
model_cls = None
|
||||
return model_cls
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/python
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from typing import Callable
|
||||
import os
|
||||
|
||||
from django.utils.translation import gettext_lazy as _, gettext, get_language
|
||||
from django.conf import settings
|
||||
|
@ -24,7 +24,7 @@ root_node_data = {
|
|||
# 第二层 view 节点,手动创建的
|
||||
view_nodes_data = [
|
||||
{'id': 'view_console', 'name': _('Console view')},
|
||||
{'id': 'view_workspace', 'name': _('Workspace view')},
|
||||
{'id': 'view_workbench', 'name': _('Workbench view')},
|
||||
{'id': 'view_audit', 'name': _('Audit view')},
|
||||
{'id': 'view_setting', 'name': _('System setting')},
|
||||
{'id': 'view_other', 'name': _('Other')},
|
||||
|
@ -55,8 +55,8 @@ extra_nodes_data = [
|
|||
{"id": "app_change_plan_node", "name": _("App change auth"), "pId": "accounts"},
|
||||
{"id": "asset_change_plan_node", "name": _("Asset change auth"), "pId": "accounts"},
|
||||
{"id": "terminal_node", "name": _("Terminal setting"), "pId": "view_setting"},
|
||||
{'id': "my_assets", "name": _("My assets"), "pId": "view_workspace"},
|
||||
{'id': "my_apps", "name": _("My apps"), "pId": "view_workspace"},
|
||||
{'id': "my_assets", "name": _("My assets"), "pId": "view_workbench"},
|
||||
{'id': "my_apps", "name": _("My apps"), "pId": "view_workbench"},
|
||||
]
|
||||
|
||||
# 将 model 放到其它节点下,而不是本来的 app 中
|
||||
|
@ -87,10 +87,9 @@ special_pid_mapper = {
|
|||
'terminal.status': 'terminal_node',
|
||||
'terminal.task': 'terminal_node',
|
||||
'audits.ftplog': 'terminal',
|
||||
'rbac.menupermission': 'view_other',
|
||||
'perms.view_myassets': 'my_assets',
|
||||
'perms.view_myapps': 'my_apps',
|
||||
'ops.add_commandexecution': 'view_workspace',
|
||||
'ops.add_commandexecution': 'view_workbench',
|
||||
'ops.view_commandexecution': 'audits',
|
||||
"perms.view_mykubernetsapp": "my_apps",
|
||||
"perms.connect_mykubernetsapp": "my_apps",
|
||||
|
@ -103,9 +102,9 @@ special_pid_mapper = {
|
|||
"settings.view_setting": "view_setting",
|
||||
"rbac.view_console": "view_console",
|
||||
"rbac.view_audit": "view_audit",
|
||||
"rbac.view_workspace": "view_workspace",
|
||||
"rbac.view_webterminal": "view_workspace",
|
||||
"rbac.view_filemanager": "view_workspace",
|
||||
"rbac.view_workbench": "view_workbench",
|
||||
"rbac.view_webterminal": "view_workbench",
|
||||
"rbac.view_filemanager": "view_workbench",
|
||||
'tickets.view_ticket': 'tickets'
|
||||
}
|
||||
|
||||
|
|
|
@ -195,7 +195,9 @@ class LDAPUserImportAPI(APIView):
|
|||
def get_ldap_users(self):
|
||||
username_list = self.request.data.get('username_list', [])
|
||||
cache_police = self.request.query_params.get('cache_police', True)
|
||||
if cache_police in LDAP_USE_CACHE_FLAGS:
|
||||
if '*' in username_list:
|
||||
users = LDAPServerUtil().search()
|
||||
elif cache_police in LDAP_USE_CACHE_FLAGS:
|
||||
users = LDAPCacheUtil().search(search_users=username_list)
|
||||
else:
|
||||
users = LDAPServerUtil().search(search_users=username_list)
|
||||
|
@ -234,4 +236,3 @@ class LDAPCacheRefreshAPI(generics.RetrieveAPIView):
|
|||
logger.error(str(e))
|
||||
return Response(data={'msg': str(e)}, status=400)
|
||||
return Response(data={'msg': 'success'})
|
||||
|
||||
|
|
|
@ -30,19 +30,15 @@ class PublicSettingApi(generics.RetrieveAPIView):
|
|||
def get_object(self):
|
||||
instance = {
|
||||
"data": {
|
||||
# Security
|
||||
"WINDOWS_SKIP_ALL_MANUAL_PASSWORD": settings.WINDOWS_SKIP_ALL_MANUAL_PASSWORD,
|
||||
"OLD_PASSWORD_HISTORY_LIMIT_COUNT": settings.OLD_PASSWORD_HISTORY_LIMIT_COUNT,
|
||||
"SECURITY_MAX_IDLE_TIME": settings.SECURITY_MAX_IDLE_TIME,
|
||||
"XPACK_ENABLED": settings.XPACK_ENABLED,
|
||||
"SECURITY_VIEW_AUTH_NEED_MFA": settings.SECURITY_VIEW_AUTH_NEED_MFA,
|
||||
"SECURITY_MFA_VERIFY_TTL": settings.SECURITY_MFA_VERIFY_TTL,
|
||||
"OLD_PASSWORD_HISTORY_LIMIT_COUNT": settings.OLD_PASSWORD_HISTORY_LIMIT_COUNT,
|
||||
"SECURITY_COMMAND_EXECUTION": settings.SECURITY_COMMAND_EXECUTION,
|
||||
"SECURITY_PASSWORD_EXPIRATION_TIME": settings.SECURITY_PASSWORD_EXPIRATION_TIME,
|
||||
"SECURITY_LUNA_REMEMBER_AUTH": settings.SECURITY_LUNA_REMEMBER_AUTH,
|
||||
"XPACK_LICENSE_IS_VALID": has_valid_xpack_license(),
|
||||
"XPACK_LICENSE_INFO": get_xpack_license_info(),
|
||||
"LOGIN_TITLE": self.get_login_title(),
|
||||
"LOGO_URLS": self.get_logo_urls(),
|
||||
"PASSWORD_RULE": {
|
||||
'SECURITY_PASSWORD_MIN_LENGTH': settings.SECURITY_PASSWORD_MIN_LENGTH,
|
||||
'SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH': settings.SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH,
|
||||
|
@ -51,16 +47,27 @@ class PublicSettingApi(generics.RetrieveAPIView):
|
|||
'SECURITY_PASSWORD_NUMBER': settings.SECURITY_PASSWORD_NUMBER,
|
||||
'SECURITY_PASSWORD_SPECIAL_CHAR': settings.SECURITY_PASSWORD_SPECIAL_CHAR,
|
||||
},
|
||||
'SECURITY_WATERMARK_ENABLED': settings.SECURITY_WATERMARK_ENABLED,
|
||||
'SECURITY_SESSION_SHARE': settings.SECURITY_SESSION_SHARE,
|
||||
# XPACK
|
||||
"XPACK_ENABLED": settings.XPACK_ENABLED,
|
||||
"XPACK_LICENSE_IS_VALID": has_valid_xpack_license(),
|
||||
"XPACK_LICENSE_INFO": get_xpack_license_info(),
|
||||
# Performance
|
||||
"LOGIN_TITLE": self.get_login_title(),
|
||||
"LOGO_URLS": self.get_logo_urls(),
|
||||
"HELP_DOCUMENT_URL": settings.HELP_DOCUMENT_URL,
|
||||
"HELP_SUPPORT_URL": settings.HELP_SUPPORT_URL,
|
||||
# Auth
|
||||
"AUTH_WECOM": settings.AUTH_WECOM,
|
||||
"AUTH_DINGTALK": settings.AUTH_DINGTALK,
|
||||
"AUTH_FEISHU": settings.AUTH_FEISHU,
|
||||
'SECURITY_WATERMARK_ENABLED': settings.SECURITY_WATERMARK_ENABLED,
|
||||
'SECURITY_SESSION_SHARE': settings.SECURITY_SESSION_SHARE,
|
||||
# Terminal
|
||||
"XRDP_ENABLED": settings.XRDP_ENABLED,
|
||||
# Announcement
|
||||
"ANNOUNCEMENT_ENABLED": settings.ANNOUNCEMENT_ENABLED,
|
||||
"ANNOUNCEMENT": settings.ANNOUNCEMENT,
|
||||
"HELP_DOCUMENT_URL": settings.HELP_DOCUMENT_URL,
|
||||
"HELP_SUPPORT_URL": settings.HELP_SUPPORT_URL,
|
||||
"AUTH_TEMP_TOKEN": settings.AUTH_TEMP_TOKEN,
|
||||
}
|
||||
}
|
||||
return instance
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import json
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
|
@ -10,10 +9,19 @@ __all__ = [
|
|||
class CASSettingSerializer(serializers.Serializer):
|
||||
AUTH_CAS = serializers.BooleanField(required=False, label=_('Enable CAS Auth'))
|
||||
CAS_SERVER_URL = serializers.CharField(required=False, max_length=1024, label=_('Server url'))
|
||||
CAS_ROOT_PROXIED_AS = serializers.CharField(required=False, allow_null=True, allow_blank=True, max_length=1024, label=_('Proxy server url'))
|
||||
CAS_ROOT_PROXIED_AS = serializers.CharField(
|
||||
required=False, allow_null=True, allow_blank=True,
|
||||
max_length=1024, label=_('Proxy server url')
|
||||
)
|
||||
CAS_LOGOUT_COMPLETELY = serializers.BooleanField(required=False, label=_('Logout completely'))
|
||||
CAS_VERSION = serializers.IntegerField(required=False, label=_('Version'), min_value=1, max_value=3)
|
||||
CAS_USERNAME_ATTRIBUTE = serializers.CharField(required=False, max_length=1024, label=_('Username attr'))
|
||||
CAS_APPLY_ATTRIBUTES_TO_USER = serializers.BooleanField(required=False, label=_('Enable attributes map'))
|
||||
CAS_VERSION = serializers.IntegerField(
|
||||
required=False, label=_('Version'), min_value=1, max_value=3
|
||||
)
|
||||
CAS_USERNAME_ATTRIBUTE = serializers.CharField(
|
||||
required=False, max_length=1024, label=_('Username attr')
|
||||
)
|
||||
CAS_APPLY_ATTRIBUTES_TO_USER = serializers.BooleanField(
|
||||
required=False, label=_('Enable attributes map')
|
||||
)
|
||||
CAS_RENAME_ATTRIBUTES = serializers.DictField(required=False, label=_('Rename attr'))
|
||||
CAS_CREATE_USER = serializers.BooleanField(required=False, label=_('Create user if not'))
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
|
@ -40,8 +39,9 @@ class LDAPSettingSerializer(serializers.Serializer):
|
|||
help_text=_('eg: ldap://localhost:389')
|
||||
)
|
||||
AUTH_LDAP_BIND_DN = serializers.CharField(required=False, max_length=1024, label=_('Bind DN'))
|
||||
AUTH_LDAP_BIND_PASSWORD = serializers.CharField(max_length=1024, write_only=True, required=False,
|
||||
label=_('Password'))
|
||||
AUTH_LDAP_BIND_PASSWORD = serializers.CharField(
|
||||
max_length=1024, write_only=True, required=False, label=_('Password')
|
||||
)
|
||||
AUTH_LDAP_SEARCH_OU = serializers.CharField(
|
||||
max_length=1024, allow_blank=True, required=False, label=_('User OU'),
|
||||
help_text=_('Use | split multi OUs')
|
||||
|
@ -55,6 +55,9 @@ class LDAPSettingSerializer(serializers.Serializer):
|
|||
help_text=_('User attr map present how to map LDAP user attr to '
|
||||
'jumpserver, username,name,email is jumpserver attr')
|
||||
)
|
||||
AUTH_LDAP_SYNC_ORG_ID = serializers.CharField(
|
||||
required=False, label=_('Organization'), max_length=36
|
||||
)
|
||||
AUTH_LDAP_SYNC_IS_PERIODIC = serializers.BooleanField(
|
||||
required=False, label=_('Periodic perform')
|
||||
)
|
||||
|
|
|
@ -22,17 +22,16 @@ class TerminalSettingSerializer(serializers.Serializer):
|
|||
help_text=_('Tips: If use other auth method, like AD/LDAP, you should disable this to '
|
||||
'avoid being able to log in after deleting')
|
||||
)
|
||||
TERMINAL_ASSET_LIST_SORT_BY = serializers.ChoiceField(SORT_BY_CHOICES, required=False, label=_('List sort by'))
|
||||
TERMINAL_ASSET_LIST_PAGE_SIZE = serializers.ChoiceField(PAGE_SIZE_CHOICES, required=False,
|
||||
label=_('List page size'))
|
||||
TERMINAL_ASSET_LIST_SORT_BY = serializers.ChoiceField(
|
||||
SORT_BY_CHOICES, required=False, label=_('List sort by')
|
||||
)
|
||||
TERMINAL_ASSET_LIST_PAGE_SIZE = serializers.ChoiceField(
|
||||
PAGE_SIZE_CHOICES, required=False, label=_('List page size')
|
||||
)
|
||||
TERMINAL_TELNET_REGEX = serializers.CharField(
|
||||
allow_blank=True, max_length=1024, required=False, label=_('Telnet login regex'),
|
||||
help_text=_("The login success message varies with devices. "
|
||||
"if you cannot log in to the device through Telnet, set this parameter")
|
||||
)
|
||||
TERMINAL_RDP_ADDR = serializers.CharField(
|
||||
required=False, label=_("RDP address"), max_length=1024, allow_blank=True,
|
||||
help_text=_('RDP visit address, eg: dev.jumpserver.org:3389')
|
||||
)
|
||||
|
||||
TERMINAL_MAGNUS_ENABLED = serializers.BooleanField(label=_("Enable database proxy"))
|
||||
XRDP_ENABLED = serializers.BooleanField(label=_("Enable XRDP"))
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
import json
|
||||
import threading
|
||||
|
||||
from django.conf import LazySettings
|
||||
from django.db.utils import ProgrammingError, OperationalError
|
||||
from django.dispatch import receiver
|
||||
from django.db.models.signals import post_save, pre_save
|
||||
from django.utils.functional import LazyObject
|
||||
|
@ -85,3 +87,18 @@ def subscribe_settings_change(sender, **kwargs):
|
|||
t = threading.Thread(target=keep_subscribe_settings_change)
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
|
||||
@receiver(django_ready)
|
||||
def monkey_patch_settings(sender, **kwargs):
|
||||
def monkey_patch_getattr(self, name):
|
||||
val = getattr(self._wrapped, name)
|
||||
# 只解析 defaults 中的 callable
|
||||
if callable(val) and val.__module__.endswith('jumpserver.conf'):
|
||||
val = val()
|
||||
return val
|
||||
|
||||
try:
|
||||
LazySettings.__getattr__ = monkey_patch_getattr
|
||||
except (ProgrammingError, OperationalError):
|
||||
pass
|
||||
|
|
|
@ -1,54 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block help_message %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="wrapper wrapper-content">
|
||||
<div class="row">
|
||||
<div class="col-sm-3" id="split-left" style="padding-left: 3px;padding-right: 0">
|
||||
{% include 'assets/_node_tree.html' %}
|
||||
</div>
|
||||
<div class="col-sm-9 animated fadeInRight" id="split-right">
|
||||
<div class="tree-toggle" style="z-index: 10">
|
||||
<div class="btn btn-sm btn-primary tree-toggle-btn" onclick="toggleSpliter()">
|
||||
<i class="fa fa-angle-left fa-x" id="toggle-icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mail-box-header">
|
||||
{% block table_container %}
|
||||
<table class="table table-striped table-bordered table-hover" id="{% block table_id %}editable{% endblock %}" >
|
||||
<thead>
|
||||
<tr>
|
||||
{% block table_head %} {% endblock %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% block table_body %} {% endblock %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
var showTree = 1;
|
||||
function toggleSpliter() {
|
||||
if (showTree === 1) {
|
||||
$("#split-left").hide(500, function () {
|
||||
$("#split-right").attr("class", "col-sm-12");
|
||||
$("#toggle-icon").attr("class", "fa fa-angle-right fa-x");
|
||||
showTree = 1;
|
||||
});
|
||||
} else {
|
||||
$("#split-right").attr("class", "col-sm-9");
|
||||
$("#toggle-icon").attr("class", "fa fa-angle-left fa-x");
|
||||
$("#split-left").show(500);
|
||||
showTree = 0;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -1,43 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load bootstrap3 %}
|
||||
{% block custom_head_css_js %}
|
||||
<script type="text/javascript" src="{% static 'js/pwstrength-bootstrap.js' %}"></script>
|
||||
{% block custom_head_css_js_create %} {% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="wrapper wrapper-content animated fadeInRight">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<h5>{{ action }}</h5>
|
||||
<div class="ibox-tools">
|
||||
<a class="collapse-link">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</a>
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||
<i class="fa fa-wrench"></i>
|
||||
</a>
|
||||
<a class="close-link">
|
||||
<i class="fa fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
{% if form.errors.all %}
|
||||
<div class="alert alert-danger" style="margin: 20px auto 0px">
|
||||
{{ form.errors.all }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% block form %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% block content %}
|
||||
<div class="wrapper wrapper-content animated fadeInRight">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<h5>
|
||||
{{ action }}
|
||||
</h5>
|
||||
<div class="ibox-tools">
|
||||
<a class="collapse-link">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</a>
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||
<i class="fa fa-wrench"></i>
|
||||
</a>
|
||||
<a class="close-link">
|
||||
<i class="fa fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
<div class="" id="content_start">
|
||||
{% block content_left_head %} {% endblock %}
|
||||
{% block table_search %}
|
||||
<form id="search_form" method="get" action="" class="pull-right mail-search form-inline">
|
||||
{% block search_form %}
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control input-sm" name="keyword" placeholder="Search" value="{{ keyword }}">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<div class="input-group-btn">
|
||||
<button id='search_btn' type="submit" class="btn btn-sm btn-primary">
|
||||
{% trans 'Search' %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% block table_container %}
|
||||
<table class="table table-striped table-bordered table-hover" id="editable" >
|
||||
<thead>
|
||||
<tr>
|
||||
{% block table_head %} {% endblock %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% block table_body %} {% endblock %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
<div class="row">
|
||||
<div class="col-sm-4">
|
||||
{% block content_bottom_left %} {% endblock %}
|
||||
</div>
|
||||
{% block table_pagination %}
|
||||
{% include '_pagination.html' %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -52,6 +52,12 @@
|
|||
<span> English</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a id="switch_ja" href="{% url 'i18n-switch' lang='ja' %}">
|
||||
<i class="fa fa-flag-checkered"></i>
|
||||
<span> 日本語</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
|
|
|
@ -1,66 +0,0 @@
|
|||
{% load i18n %}
|
||||
{% load common_tags %}
|
||||
{% if is_paginated %}
|
||||
<div class="col-sm-4">
|
||||
<div class="dataTables_info text-center" id="editable_info" role="status" aria-live="polite">
|
||||
{# 显示第 {{ page_obj.start_index }} 至 {{ page_obj.end_index }} 项结果,共 {{ paginator.count }} 项#}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="dataTables_paginate paging_simple_numbers" id="editable_paginate">
|
||||
<ul class="pagination" style="margin-top: 0; float: right">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="paginate_button previous" aria-controls="editable" tabindex="0" id="previous">
|
||||
<a data-page="next" class="page" href="?page={{ page_obj.previous_page_number}}">‹</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for page in paginator.num_pages|pagination_range:page_obj.number %}
|
||||
{% if page == page_obj.number %}
|
||||
<li class="paginate_button active" aria-controls="editable" tabindex="0">
|
||||
{% else %}
|
||||
<li class="paginate_button" aria-controls="editable" tabindex="0">
|
||||
{% endif %}
|
||||
<a class="page" href="?page={{ page }}" title="第{{ page }}页">{{ page }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="paginate_button next" aria-controls="editable" tabindex="0" id="next">
|
||||
<a data-page="next" class="page" href="?page={{ page_obj.next_page_number }}">›</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$('.page').click(function () {
|
||||
var searchStr = location.search;
|
||||
var old_href = $(this).attr('href').replace('?', '');
|
||||
var searchArray = searchStr.split('&');
|
||||
|
||||
if (searchStr === '') {
|
||||
searchStr = '?page=1'
|
||||
}
|
||||
|
||||
if (searchStr.indexOf('page') >= 0) {
|
||||
searchArray.pop();
|
||||
}
|
||||
|
||||
searchArray.push(old_href);
|
||||
if (searchArray.length > 1) {
|
||||
$(this).attr('href', searchArray.join('&'));
|
||||
}
|
||||
})
|
||||
|
||||
$('#editable_info').html(
|
||||
"{% trans 'Displays the results of items _START_ to _END_; A total of _TOTAL_ entries' %}"
|
||||
.replace('_START_', {{ page_obj.start_index }})
|
||||
.replace('_END_', {{ page_obj.end_index }})
|
||||
.replace('_TOTAL_', {{ paginator.count }})
|
||||
)
|
||||
});
|
||||
|
||||
</script>
|
|
@ -1,15 +0,0 @@
|
|||
{% load i18n %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{% trans 'Confirm delete' %}</title>
|
||||
</head>
|
||||
<body>
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<p>{% trans 'Are you sure delete' %} <b>{{ object.name }} </b> ?</p>
|
||||
<input type="submit" value="Confirm" />
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
|
@ -1,618 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% block content %}
|
||||
<div class="wrapper wrapper-content">
|
||||
<div class="row">
|
||||
<div class="col-sm-3">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<span class="label label-success pull-right">Users</span>
|
||||
<h5>{% trans 'Total users' %}</h5>
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
<h1 class="no-margins"><a href="{% url 'users:user-list' %}"><span id="total_count_users"></span></a></h1>
|
||||
<small>All users</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<span class="label label-info pull-right">Assets</span>
|
||||
<h5>{% trans 'Total assets' %}</h5>
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
<h1 class="no-margins"><a href="{% url 'assets:asset-list' %}"><span id="total_count_assets"></span></a></h1>
|
||||
<small>All assets</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-3">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<span class="label label-primary pull-right">Online</span>
|
||||
<h5>{% trans 'Online users' %}</h5>
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
<h1 class="no-margins"><a href="{% url 'terminal:session-online-list' %}"> <span id="total_count_online_users"></span></a></h1>
|
||||
<small>Online users</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-3">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<span class="label label-danger pull-right">Connected</span>
|
||||
<h5>{% trans 'Online sessions' %}</h5>
|
||||
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
<h1 class="no-margins"><a href="{% url 'terminal:session-online-list' %}"> <span id="total_count_online_sessions"></span></a></h1>
|
||||
<small>Online sessions</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-2 border-bottom white-bg dashboard-header" style="margin-left:15px;height: 346px">
|
||||
<small>{% trans 'In the past week, a total of ' %}<span class="text-info" id="dates_total_count_login_users"></span>{% trans ' users have logged in ' %}<span class="text-success" id="dates_total_count_login_times"></span>{% trans ' times asset.' %}</small>
|
||||
<ul class="list-group clear-list m-t" id="dates_login_times_top5_users">
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-sm-7" id="dates_metrics_echarts" style="margin-left: -15px;height: 346px;padding: 15px 0 15px 0;"></div>
|
||||
<div class="col-sm-3 white-bg" id="top1" style="margin-left: -15px;height: 346px">
|
||||
<div class="statistic-box">
|
||||
<h4>
|
||||
{% trans 'Active user asset ratio' %}
|
||||
</h4>
|
||||
<p>
|
||||
{% trans 'The following graphs describe the percentage of active users per month and assets per user host per month, respectively.' %}
|
||||
</p>
|
||||
<div class="row text-center">
|
||||
<div class="col-sm-6">
|
||||
<div id="dates_total_count_users_pie" style="width: 140px; height: 140px;">
|
||||
</div>
|
||||
<h5>{% trans 'User' %}</h5>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div id="dates_total_count_assets_pie" style="width: 140px; height: 140px;"></div>
|
||||
<h5>{% trans 'Asset' %}</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div class="m-t">
|
||||
<small></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-4">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<h5>{% trans 'Top 10 assets in a week' %}</h5>
|
||||
<div class="ibox-tools">
|
||||
<a class="collapse-link">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</a>
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||
<i class="fa fa-wrench"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-user"></ul>
|
||||
<a class="close-link">
|
||||
<i class="fa fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ibox-content ibox-heading">
|
||||
<h3><i class="fa fa-inbox"></i>{% trans 'Top 10 assets in a week'%}</h3>
|
||||
<small><i class="fa fa-map-marker"></i>{% trans 'Login frequency and last login record.' %}</small>
|
||||
</div>
|
||||
<div class="ibox-content inspinia-timeline" id="dates_login_times_top10_assets">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<h5>{% trans 'Last 10 login' %}</h5>
|
||||
<div class="ibox-tools">
|
||||
<span class="label label-info-light">10 Messages</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ibox-content ibox-heading">
|
||||
<h3><i class="fa fa-paper-plane-o"></i> {% trans 'Login record' %}</h3>
|
||||
<small><i class="fa fa-map-marker"></i>{% trans 'Last 10 login records.' %}</small>
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
<div>
|
||||
<div class="feed-activity-list" id="dates_login_record_top10_sessions">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-4">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<h5>{% trans 'Top 10 users in a week' %}</h5>
|
||||
<div class="ibox-tools">
|
||||
<a class="collapse-link">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</a>
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||
<i class="fa fa-wrench"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-user"></ul>
|
||||
<a class="close-link">
|
||||
<i class="fa fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ibox-content ibox-heading">
|
||||
<h3><i class="fa fa-user"></i>{% trans 'Top 10 users in a week' %}</h3>
|
||||
<small><i class="fa fa-map-marker"></i>{% trans 'User login frequency and last login record.' %}</small>
|
||||
</div>
|
||||
<div class="ibox-content inspinia-timeline" id="dates_login_times_top10_users">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block custom_foot_js %}
|
||||
<script src="{% static 'js/plugins/echarts/echarts.js' %}"></script>
|
||||
<script>
|
||||
|
||||
function requireMonthMetricsECharts(data){
|
||||
require(
|
||||
[
|
||||
'echarts',
|
||||
'echarts/chart/line'
|
||||
],
|
||||
function (ec) {
|
||||
var monthMetricsECharts = ec.init(document.getElementById('dates_metrics_echarts'));
|
||||
var option = {
|
||||
title : {
|
||||
text: "{% trans 'Monthly data overview' %}",
|
||||
subtext: "{% trans 'History summary in one month' %}",
|
||||
x: 'center'
|
||||
},
|
||||
tooltip : {
|
||||
trigger: 'axis'
|
||||
},
|
||||
backgroundColor: '#fff',
|
||||
legend: {
|
||||
data:["{% trans 'Login count' %}", "{% trans 'Active users' %}", "{% trans 'Active assets' %}"],
|
||||
y: 'bottom'
|
||||
},
|
||||
toolbox: {
|
||||
show : false,
|
||||
feature : {
|
||||
magicType : {show: true, type: ['line', 'bar']}
|
||||
}
|
||||
},
|
||||
calculable : true,
|
||||
xAxis : [
|
||||
{
|
||||
type : 'category',
|
||||
boundaryGap : false,
|
||||
data : data['dates_metrics_date'],
|
||||
}
|
||||
],
|
||||
yAxis : [
|
||||
{
|
||||
type : 'value'
|
||||
}
|
||||
],
|
||||
series : [
|
||||
{
|
||||
name: "{% trans 'Login count' %}",
|
||||
type:'line',
|
||||
smooth: true,
|
||||
itemStyle: {normal: {areaStyle: {type: 'default'}}},
|
||||
data: data['dates_metrics_total_count_login']
|
||||
},
|
||||
{
|
||||
name: "{% trans 'Active users' %}",
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
itemStyle: {normal: {areaStyle: {type: 'default'}}},
|
||||
data: data['dates_metrics_total_count_active_users']
|
||||
},
|
||||
{
|
||||
name:"{% trans 'Active assets' %}",
|
||||
type:'line',
|
||||
smooth:true,
|
||||
itemStyle: {normal: {areaStyle: {type: 'default'}}},
|
||||
data: data['dates_metrics_total_count_active_assets']
|
||||
}
|
||||
]
|
||||
};
|
||||
monthMetricsECharts.setOption(option);
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
function requireMonthTotalCountUsersPie(data){
|
||||
require(
|
||||
[
|
||||
'echarts',
|
||||
'echarts/chart/pie'
|
||||
],
|
||||
function (ec) {
|
||||
var monthTotalCountUsersPie = ec.init(document.getElementById('dates_total_count_users_pie'));
|
||||
var option = {
|
||||
tooltip : {
|
||||
trigger: 'item',
|
||||
formatter: "{b} <br> {c} <br> ({d}%)"
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
orient : 'vertical',
|
||||
x : 'left',
|
||||
data:["{% trans 'Monthly active users' %}", "{% trans 'Disable user' %}", "{% trans 'Month not logged in user' %}"]
|
||||
},
|
||||
toolbox: {
|
||||
show : false,
|
||||
feature : {
|
||||
mark : {show: true},
|
||||
dataView : {show: true, readOnly: false},
|
||||
magicType : {
|
||||
show: true,
|
||||
type: ['pie', 'funnel'],
|
||||
option: {
|
||||
funnel: {
|
||||
x: '25%',
|
||||
width: '50%',
|
||||
funnelAlign: 'center',
|
||||
max: 1548
|
||||
}
|
||||
}
|
||||
},
|
||||
restore : {show: true},
|
||||
saveAsImage : {show: true}
|
||||
}
|
||||
},
|
||||
calculable : true,
|
||||
series : [
|
||||
{
|
||||
name:"{% trans 'Access to the source' %}",
|
||||
type:'pie',
|
||||
radius : ['50%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle : {
|
||||
normal : {
|
||||
label : {
|
||||
show : false
|
||||
},
|
||||
labelLine : {
|
||||
show : false
|
||||
}
|
||||
},
|
||||
emphasis : {
|
||||
label : {
|
||||
show : true,
|
||||
position : 'center',
|
||||
textStyle : {
|
||||
fontSize : '5',
|
||||
fontWeight : 'bold'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
data:[
|
||||
{value:data['dates_total_count_active_users'], name:"{% trans 'Monthly active users' %}"},
|
||||
{value:data['dates_total_count_disabled_users'], name:"{% trans 'Disable user' %}"},
|
||||
{value:data['dates_total_count_inactive_users'], name:"{% trans 'Month not logged in user' %}"}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
monthTotalCountUsersPie.setOption(option);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function requireMonthTotalCountAssetsPie(data){
|
||||
require(
|
||||
[
|
||||
'echarts',
|
||||
'echarts/chart/pie'
|
||||
],
|
||||
function (ec) {
|
||||
var monthTotalCountAssetsPie = ec.init(document.getElementById('dates_total_count_assets_pie'));
|
||||
var option = {
|
||||
tooltip : {
|
||||
trigger: 'item',
|
||||
formatter: "{b} <br> {c} <br> ({d}%)"
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
orient : 'vertical',
|
||||
x : 'left',
|
||||
data:["{% trans 'Month is logged into the asset' %}", "{% trans 'Disable host' %}", "{% trans 'Month not logged on host' %}"]
|
||||
},
|
||||
toolbox: {
|
||||
show : false,
|
||||
feature : {
|
||||
mark : {show: true},
|
||||
dataView : {show: true, readOnly: false},
|
||||
magicType : {
|
||||
show: true,
|
||||
type: ['pie', 'funnel'],
|
||||
option: {
|
||||
funnel: {
|
||||
x: '25%',
|
||||
width: '50%',
|
||||
funnelAlign: 'center',
|
||||
max: 1548
|
||||
}
|
||||
}
|
||||
},
|
||||
restore : {show: true},
|
||||
saveAsImage : {show: true}
|
||||
}
|
||||
},
|
||||
calculable : true,
|
||||
series : [
|
||||
{
|
||||
name:"{% trans 'Access to the source' %}",
|
||||
type:'pie',
|
||||
radius : ['50%', '70%'],
|
||||
itemStyle : {
|
||||
normal : {
|
||||
label : {
|
||||
show : false
|
||||
},
|
||||
labelLine : {
|
||||
show : false
|
||||
}
|
||||
},
|
||||
emphasis : {
|
||||
label : {
|
||||
show : true,
|
||||
position : 'center',
|
||||
textStyle : {
|
||||
fontSize : '5',
|
||||
fontWeight : 'bold'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
data:[
|
||||
{value:data['dates_total_count_active_assets'], name:"{% trans 'Month is logged into the host' %}"},
|
||||
{value:data['dates_total_count_disabled_assets'], name:"{% trans 'Disable host' %}"},
|
||||
{value:data['dates_total_count_inactive_assets'], name:"{% trans 'Month not logged on host' %}"}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
monthTotalCountAssetsPie.setOption(option);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
var indexUrl = "/api/v1/index/";
|
||||
|
||||
function renderRequestApi(query, success, error){
|
||||
var url = indexUrl + "?" + query;
|
||||
if (!error){
|
||||
error = function (){console.log("Request url error: " + url)}
|
||||
}
|
||||
requestApi({
|
||||
url: url,
|
||||
method: "GET",
|
||||
success: success,
|
||||
error: error,
|
||||
flash_message: false,
|
||||
})
|
||||
}
|
||||
|
||||
function renderTotalCount(){
|
||||
var success = function (data) {
|
||||
$('#total_count_assets').html(data['total_count_assets']);
|
||||
$('#total_count_users').html(data['total_count_users']);
|
||||
$('#total_count_online_users').html(data['total_count_online_users']);
|
||||
$('#total_count_online_sessions').html(data['total_count_online_sessions']);
|
||||
};
|
||||
renderRequestApi('total_count=1', success);
|
||||
}
|
||||
|
||||
function renderMonthMetricsECharts(){
|
||||
var success = function (data) {
|
||||
requireMonthMetricsECharts(data)
|
||||
};
|
||||
renderRequestApi('dates_metrics=1', success)
|
||||
}
|
||||
|
||||
function renderMonthTotalCountUsersPie(){
|
||||
var success = function (data) {
|
||||
requireMonthTotalCountUsersPie(data)
|
||||
};
|
||||
renderRequestApi('dates_total_count_users=1', success)
|
||||
|
||||
}
|
||||
|
||||
function renderMonthTotalCountAssetsPie(){
|
||||
var success = function (data) {
|
||||
requireMonthTotalCountAssetsPie(data)
|
||||
};
|
||||
renderRequestApi('dates_total_count_assets=1', success)
|
||||
}
|
||||
|
||||
function renderWeekTotalCount(){
|
||||
var success = function (data) {
|
||||
$('#dates_total_count_login_users').html(data['dates_total_count_login_users']);
|
||||
$('#dates_total_count_login_times').html(data['dates_total_count_login_times'])
|
||||
};
|
||||
renderRequestApi('dates_total_count=1', success)
|
||||
}
|
||||
|
||||
function renderWeekLoginTimesTop5Users(){
|
||||
var success = function (data){
|
||||
var html = "";
|
||||
var html_cell = "" +
|
||||
"<li class=\"list-group-item fist-item\">" +
|
||||
"<span class=\"pull-right\">" +
|
||||
"{TOTAL} {% trans ' times/week' %}" +
|
||||
"</span>" +
|
||||
"<span class=\"label \">{INDEX}</span> {USER}" +
|
||||
"</li>";
|
||||
|
||||
$.each(data['dates_login_times_top5_users'], function(index, value){
|
||||
html += html_cell.replace('{TOTAL}', value['total'])
|
||||
.replace('{USER}', value['user'])
|
||||
.replace('{INDEX}', index+1)
|
||||
});
|
||||
$('#dates_login_times_top5_users').html(html)
|
||||
};
|
||||
renderRequestApi('dates_login_times_top5_users=1', success)
|
||||
}
|
||||
|
||||
function renderWeekLoginTimesTop10Assets(){
|
||||
var success = function (data){
|
||||
var html = "";
|
||||
var html_cell = "" +
|
||||
"<div class=\"timeline-item\">" +
|
||||
"<div class=\"row\">" +
|
||||
"<div class=\"col-xs-5 date ellipsis\">" +
|
||||
"<i class=\"fa fa-info-circle\"></i>" +
|
||||
"<strong data-toggle=\"tooltip\" title=\"{ASSET}\">{ASSET}</strong>" +
|
||||
"<br/>" +
|
||||
"<small class=\"text-navy\">{TOTAL}{% trans ' times' %}</small>" +
|
||||
"</div>" +
|
||||
"<div class=\"col-xs-7 content no-top-border\">" +
|
||||
"<p class=\"m-b-xs\">{% trans 'The time last logged in' %}</p>" +
|
||||
"<p>{% trans 'At' %} {DATE_LAST}</p>" +
|
||||
"</div>" +
|
||||
"</div>" +
|
||||
"</div>";
|
||||
|
||||
var assets = data['dates_login_times_top10_assets'];
|
||||
if (assets.length !== 0){
|
||||
$.each(assets, function(index, value){
|
||||
html += html_cell
|
||||
.replaceAll('{ASSET}', value['asset'])
|
||||
.replace('{TOTAL}', value['total'])
|
||||
.replace('{DATE_LAST}', toSafeLocalDateStr(value['last']))
|
||||
});
|
||||
}
|
||||
else{
|
||||
html += "<p class=\"text-center\">{% trans '(No)' %}</p>"
|
||||
}
|
||||
$('#dates_login_times_top10_assets').html(html)
|
||||
};
|
||||
renderRequestApi('dates_login_times_top10_assets=1', success)
|
||||
}
|
||||
|
||||
function renderWeekLoginTimesTop10Users(){
|
||||
var success = function (data){
|
||||
var html = "";
|
||||
var html_cell = "" +
|
||||
"<div class=\"timeline-item\">" +
|
||||
"<div class=\"row\">" +
|
||||
"<div class=\"col-xs-5 date ellipsis\">" +
|
||||
"<i class=\"fa fa-info-circle\"></i>" +
|
||||
"<strong data-toggle=\"tooltip\" title=\"{USER}\">{USER}</strong>" +
|
||||
"<br/>" +
|
||||
"<small class=\"text-navy\">{TOTAL}{% trans ' times' %}</small>" +
|
||||
"</div>" +
|
||||
"<div class=\"col-xs-7 content no-top-border\">" +
|
||||
"<p class=\"m-b-xs\">{% trans 'The time last logged in' %}</p>" +
|
||||
"<p>{% trans 'At' %} {DATE_LAST}</p>" +
|
||||
"</div>" +
|
||||
"</div>" +
|
||||
"</div>";
|
||||
|
||||
var users = data['dates_login_times_top10_users'];
|
||||
if (users.length !== 0){
|
||||
$.each(users, function(index, value){
|
||||
html += html_cell.replaceAll('{USER}', value['user'])
|
||||
.replace('{TOTAL}', value['total'])
|
||||
.replace('{DATE_LAST}', toSafeLocalDateStr(value['last']))
|
||||
});
|
||||
}
|
||||
else{
|
||||
html += "<p class=\"text-center\">{% trans '(No)' %}</p>"
|
||||
}
|
||||
$('#dates_login_times_top10_users').html(html)
|
||||
};
|
||||
renderRequestApi('dates_login_times_top10_users=1', success)
|
||||
}
|
||||
|
||||
function renderWeekLoginRecordTop10Sessions(){
|
||||
var success = function (data){
|
||||
var html = "";
|
||||
var html_cell = "" +
|
||||
"<div class=\"feed-element\">" +
|
||||
"<a href=\"#\" class=\"pull-left\">" +
|
||||
"<img alt=\"image\" class=\"img-circle\" src=\"{% static 'img/avatar/user.png' %}\">" +
|
||||
"</a>" +
|
||||
"<div class=\"media-body \">" +
|
||||
"<small class=\"pull-right {TEXT_NAVY}\">{TIMESINCE} {% trans 'Before' %}</small>" +
|
||||
"<strong>{USER}</strong> {% trans 'Login in ' %}{ASSET} <br>" +
|
||||
"<small class=\"text-muted\">{DATE_START}</small>" +
|
||||
"</div>" +
|
||||
"</div>";
|
||||
|
||||
var users = data['dates_login_record_top10_sessions'];
|
||||
if (users.length !== 0){
|
||||
$.each(users, function(index, value){
|
||||
console.log(value['is_finished'])
|
||||
html += html_cell.replaceAll('{USER}', value['user'])
|
||||
.replace('{ASSET}', value['asset'])
|
||||
.replace('{DATE_START}', toSafeLocalDateStr(value['date_start']))
|
||||
.replace('{TEXT_NAVY}', value['is_finished']?'':'text-navy')
|
||||
.replace('{TIMESINCE}', value['timesince'])
|
||||
|
||||
});
|
||||
}
|
||||
else{
|
||||
html += "<p class=\"text-center\">{% trans '(No)' %}</p>"
|
||||
}
|
||||
$('#dates_login_record_top10_sessions').html(html)
|
||||
|
||||
};
|
||||
renderRequestApi('dates_login_record_top10_sessions=1', success)
|
||||
}
|
||||
|
||||
function renderData(){
|
||||
renderTotalCount();
|
||||
renderMonthMetricsECharts();
|
||||
renderMonthTotalCountUsersPie();
|
||||
renderMonthTotalCountAssetsPie();
|
||||
renderWeekTotalCount();
|
||||
renderWeekLoginTimesTop5Users();
|
||||
renderWeekLoginTimesTop10Assets();
|
||||
renderWeekLoginRecordTop10Sessions();
|
||||
renderWeekLoginTimesTop10Users();
|
||||
}
|
||||
|
||||
require.config({
|
||||
paths: {
|
||||
'echarts': '/static/js/plugins/echarts/chart/',
|
||||
'echarts/chart/line': '/static/js/plugins/echarts/chart/line',
|
||||
'echarts/chart/pie': '/static/js/plugins/echarts/chart/pie'
|
||||
}
|
||||
});
|
||||
|
||||
$(document).ready(function(){
|
||||
$('#show').click(function(){
|
||||
$('#show').css('display', 'none');
|
||||
$('#more').css('display', 'block');
|
||||
});
|
||||
$("[data-toggle='tooltip']").tooltip();
|
||||
renderData()
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
|
@ -7,3 +7,4 @@ from .task import *
|
|||
from .storage import *
|
||||
from .status import *
|
||||
from .sharing import *
|
||||
from .endpoint import *
|
||||
|
|
|
@ -1,20 +1,17 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import time
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.shortcuts import HttpResponse
|
||||
from rest_framework import generics
|
||||
from rest_framework.fields import DateTimeField
|
||||
from rest_framework.response import Response
|
||||
from django.template import loader
|
||||
|
||||
from terminal.models import CommandStorage, Session, Command
|
||||
from terminal.filters import CommandFilter
|
||||
from orgs.utils import current_org
|
||||
from common.drf.api import JMSBulkModelViewSet
|
||||
from common.utils import get_logger
|
||||
from terminal.serializers import InsecureCommandAlertSerializer
|
||||
from terminal.backends.command.serializers import InsecureCommandAlertSerializer
|
||||
from terminal.exceptions import StorageInvalid
|
||||
from ..backends import (
|
||||
get_command_storage, get_multi_command_storage,
|
||||
|
@ -23,7 +20,7 @@ from ..backends import (
|
|||
from ..notifications import CommandAlertMessage
|
||||
|
||||
logger = get_logger(__name__)
|
||||
__all__ = ['CommandViewSet', 'CommandExportApi', 'InsecureCommandAlertAPI']
|
||||
__all__ = ['CommandViewSet', 'InsecureCommandAlertAPI']
|
||||
|
||||
|
||||
class CommandQueryMixin:
|
||||
|
@ -191,29 +188,6 @@ class CommandViewSet(JMSBulkModelViewSet):
|
|||
return Response({"msg": msg}, status=401)
|
||||
|
||||
|
||||
class CommandExportApi(CommandQueryMixin, generics.ListAPIView):
|
||||
serializer_class = SessionCommandSerializer
|
||||
rbac_perms = {
|
||||
'list': 'terminal.view_command'
|
||||
}
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
|
||||
template = 'terminal/command_report.html'
|
||||
context = {
|
||||
'queryset': queryset,
|
||||
'total_count': len(queryset),
|
||||
'now': time.time(),
|
||||
}
|
||||
content = loader.render_to_string(template, context, request)
|
||||
content_type = 'application/octet-stream'
|
||||
response = HttpResponse(content, content_type)
|
||||
filename = 'command-report-{}.html'.format(int(time.time()))
|
||||
response['Content-Disposition'] = 'attachment; filename="%s"' % filename
|
||||
return response
|
||||
|
||||
|
||||
class InsecureCommandAlertAPI(generics.CreateAPIView):
|
||||
serializer_class = InsecureCommandAlertSerializer
|
||||
rbac_perms = {
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from common.drf.api import JMSBulkModelViewSet
|
||||
from common.utils import get_object_or_none
|
||||
from django.shortcuts import get_object_or_404
|
||||
from assets.models import Asset
|
||||
from orgs.utils import tmp_to_root_org
|
||||
from applications.models import Application
|
||||
from terminal.models import Session
|
||||
from ..models import Endpoint, EndpointRule
|
||||
from .. import serializers
|
||||
|
||||
|
||||
__all__ = ['EndpointViewSet', 'EndpointRuleViewSet']
|
||||
|
||||
|
||||
class EndpointViewSet(JMSBulkModelViewSet):
|
||||
filterset_fields = ('name', 'host')
|
||||
search_fields = filterset_fields
|
||||
serializer_class = serializers.EndpointSerializer
|
||||
queryset = Endpoint.objects.all()
|
||||
rbac_perms = {
|
||||
'smart': 'terminal.view_endpoint'
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_target_ip(request):
|
||||
# 用来方便测试
|
||||
target_ip = request.GET.get('target_ip')
|
||||
if target_ip:
|
||||
return target_ip
|
||||
|
||||
asset_id = request.GET.get('asset_id')
|
||||
app_id = request.GET.get('app_id')
|
||||
session_id = request.GET.get('session_id')
|
||||
token = request.GET.get('token')
|
||||
if token:
|
||||
from authentication.api.connection_token import TokenCacheMixin as TokenUtil
|
||||
value = TokenUtil().get_token_from_cache(token)
|
||||
if value:
|
||||
if value.get('type') == 'asset':
|
||||
asset_id = value.get('asset')
|
||||
else:
|
||||
app_id = value.get('application')
|
||||
if asset_id:
|
||||
pk, model = asset_id, Asset
|
||||
elif app_id:
|
||||
pk, model = app_id, Application
|
||||
elif session_id:
|
||||
pk, model = session_id, Session
|
||||
else:
|
||||
return ''
|
||||
|
||||
with tmp_to_root_org():
|
||||
instance = get_object_or_404(model, pk=pk)
|
||||
target_ip = instance.get_target_ip()
|
||||
return target_ip
|
||||
|
||||
@action(methods=['get'], detail=False, url_path='smart')
|
||||
def smart(self, request, *args, **kwargs):
|
||||
protocol = request.GET.get('protocol')
|
||||
if not protocol:
|
||||
return Response(
|
||||
data={'error': _('Not found protocol query params')},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
target_ip = self.get_target_ip(request)
|
||||
endpoint = EndpointRule.match_endpoint(target_ip, protocol, request)
|
||||
serializer = self.get_serializer(endpoint)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class EndpointRuleViewSet(JMSBulkModelViewSet):
|
||||
filterset_fields = ('name',)
|
||||
search_fields = filterset_fields
|
||||
serializer_class = serializers.EndpointRuleSerializer
|
||||
queryset = EndpointRule.objects.all()
|
|
@ -42,7 +42,6 @@ class MySessionAPIView(generics.ListAPIView):
|
|||
serializer_class = serializers.SessionSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
with tmp_to_root_org():
|
||||
user = self.request.user
|
||||
qs = Session.objects.filter(user_id=user.id)
|
||||
return qs
|
||||
|
|
|
@ -12,7 +12,7 @@ from django.utils.translation import gettext_lazy as _
|
|||
|
||||
from common.exceptions import JMSException
|
||||
from common.drf.api import JMSBulkModelViewSet
|
||||
from common.utils import get_object_or_none
|
||||
from common.utils import get_object_or_none, get_request_ip
|
||||
from common.permissions import WithBootstrapToken
|
||||
from ..models import Terminal
|
||||
from .. import serializers
|
||||
|
|
|
@ -4,6 +4,7 @@ import datetime
|
|||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
from django.db.utils import OperationalError
|
||||
from common.utils.common import pretty_string
|
||||
|
||||
from .base import CommandBase
|
||||
|
||||
|
@ -32,9 +33,11 @@ class CommandStore(CommandBase):
|
|||
"""
|
||||
_commands = []
|
||||
for c in commands:
|
||||
cmd_input = pretty_string(c['input'])
|
||||
cmd_output = pretty_string(c['output'], max_length=1024)
|
||||
_commands.append(self.model(
|
||||
user=c["user"], asset=c["asset"], system_user=c["system_user"],
|
||||
input=c["input"], output=c["output"], session=c["session"],
|
||||
input=cmd_input, output=cmd_output, session=c["session"],
|
||||
risk_level=c.get("risk_level", 0), org_id=c["org_id"],
|
||||
timestamp=c["timestamp"]
|
||||
))
|
||||
|
|
|
@ -18,7 +18,6 @@ from common.utils import get_logger
|
|||
from common.exceptions import JMSException
|
||||
from .models import AbstractSessionCommand
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
|
@ -27,7 +26,7 @@ class InvalidElasticsearch(JMSException):
|
|||
default_detail = _('Invalid elasticsearch config')
|
||||
|
||||
|
||||
class CommandStore():
|
||||
class CommandStore(object):
|
||||
def __init__(self, config):
|
||||
hosts = config.get("HOSTS")
|
||||
kwargs = config.get("OTHER", {})
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
@ -28,6 +29,10 @@ class AbstractSessionCommand(OrgModelMixin):
|
|||
class Meta:
|
||||
abstract = True
|
||||
|
||||
@lazyproperty
|
||||
def timestamp_display(self):
|
||||
return datetime.fromtimestamp(self.timestamp)
|
||||
|
||||
@lazyproperty
|
||||
def remote_addr(self):
|
||||
from terminal.models import Session
|
||||
|
|
|
@ -4,27 +4,19 @@ from rest_framework import serializers
|
|||
|
||||
from .models import AbstractSessionCommand
|
||||
|
||||
__all__ = ['SessionCommandSerializer', 'InsecureCommandAlertSerializer']
|
||||
|
||||
class SessionCommandSerializer(serializers.Serializer):
|
||||
"""使用这个类作为基础Command Log Serializer类, 用来序列化"""
|
||||
|
||||
id = serializers.UUIDField(read_only=True)
|
||||
class SimpleSessionCommandSerializer(serializers.Serializer):
|
||||
""" 简单Session命令序列类, 用来提取公共字段 """
|
||||
user = serializers.CharField(label=_("User")) # 限制 64 字符,见 validate_user
|
||||
asset = serializers.CharField(max_length=128, label=_("Asset"))
|
||||
system_user = serializers.CharField(max_length=64, label=_("System user"))
|
||||
input = serializers.CharField(max_length=128, label=_("Command"))
|
||||
output = serializers.CharField(max_length=1024, allow_blank=True, label=_("Output"))
|
||||
input = serializers.CharField(max_length=2048, label=_("Command"))
|
||||
session = serializers.CharField(max_length=36, label=_("Session ID"))
|
||||
risk_level = serializers.ChoiceField(required=False, label=_("Risk level"), choices=AbstractSessionCommand.RISK_LEVEL_CHOICES)
|
||||
risk_level_display = serializers.SerializerMethodField(label=_('Risk level display'))
|
||||
risk_level = serializers.ChoiceField(
|
||||
required=False, label=_("Risk level"), choices=AbstractSessionCommand.RISK_LEVEL_CHOICES
|
||||
)
|
||||
org_id = serializers.CharField(max_length=36, required=False, default='', allow_null=True, allow_blank=True)
|
||||
timestamp = serializers.IntegerField(label=_('Timestamp'))
|
||||
remote_addr = serializers.CharField(read_only=True, label=_('Remote Address'))
|
||||
|
||||
@staticmethod
|
||||
def get_risk_level_display(obj):
|
||||
risk_mapper = dict(AbstractSessionCommand.RISK_LEVEL_CHOICES)
|
||||
return risk_mapper.get(obj.risk_level)
|
||||
|
||||
def validate_user(self, value):
|
||||
if len(value) > 64:
|
||||
|
@ -32,9 +24,22 @@ class SessionCommandSerializer(serializers.Serializer):
|
|||
return value
|
||||
|
||||
|
||||
class InsecureCommandAlertSerializer(serializers.Serializer):
|
||||
input = serializers.CharField()
|
||||
asset = serializers.CharField()
|
||||
user = serializers.CharField()
|
||||
risk_level = serializers.IntegerField()
|
||||
session = serializers.UUIDField()
|
||||
class InsecureCommandAlertSerializer(SimpleSessionCommandSerializer):
|
||||
pass
|
||||
|
||||
|
||||
class SessionCommandSerializer(SimpleSessionCommandSerializer):
|
||||
"""使用这个类作为基础Command Log Serializer类, 用来序列化"""
|
||||
|
||||
id = serializers.UUIDField(read_only=True)
|
||||
system_user = serializers.CharField(max_length=64, label=_("System user"))
|
||||
output = serializers.CharField(max_length=2048, allow_blank=True, label=_("Output"))
|
||||
risk_level_display = serializers.SerializerMethodField(label=_('Risk level display'))
|
||||
timestamp = serializers.IntegerField(label=_('Timestamp'))
|
||||
timestamp_display = serializers.DateTimeField(label=_('Datetime'))
|
||||
remote_addr = serializers.CharField(read_only=True, label=_('Remote Address'))
|
||||
|
||||
@staticmethod
|
||||
def get_risk_level_display(obj):
|
||||
risk_mapper = dict(AbstractSessionCommand.RISK_LEVEL_CHOICES)
|
||||
return risk_mapper.get(obj.risk_level)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue