Merge pull request #8054 from jumpserver/dev

v2.21.0-rc1
pull/8133/head
Jiangjie.Bai 2022-04-13 20:25:47 +08:00 committed by GitHub
commit 21c41a6334
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
143 changed files with 9221 additions and 3156 deletions

18
.github/workflows/lgtm.yml vendored Normal file
View File

@ -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$"]'

View File

@ -1,4 +1,3 @@
from .application import * from .application import *
from .account import * from .account import *
from .mixin import *
from .remote_app import * from .remote_app import *

View File

@ -41,7 +41,7 @@ class AppType(models.TextChoices):
def category_types_mapper(cls): def category_types_mapper(cls):
return { return {
AppCategory.db: [ AppCategory.db: [
cls.mysql, cls.oracle, cls.pgsql, cls.mariadb, cls.mysql, cls.mariadb, cls.oracle, cls.pgsql,
cls.sqlserver, cls.redis, cls.mongodb cls.sqlserver, cls.redis, cls.mongodb
], ],
AppCategory.remote_app: [ AppCategory.remote_app: [

View File

@ -8,6 +8,7 @@ from django.conf import settings
from orgs.mixins.models import OrgModelMixin from orgs.mixins.models import OrgModelMixin
from common.mixins import CommonModelMixin from common.mixins import CommonModelMixin
from common.tree import TreeNode from common.tree import TreeNode
from common.utils import is_uuid
from assets.models import Asset, SystemUser from assets.models import Asset, SystemUser
from ..utils import KubernetesTree from ..utils import KubernetesTree
@ -19,6 +20,7 @@ class ApplicationTreeNodeMixin:
name: str name: str
type: str type: str
category: str category: str
attrs: dict
@staticmethod @staticmethod
def create_tree_id(pid, type, v): def create_tree_id(pid, type, v):
@ -99,6 +101,7 @@ class ApplicationTreeNodeMixin:
temp_pid = pid temp_pid = pid
type_category_mapper = const.AppType.type_category_mapper() type_category_mapper = const.AppType.type_category_mapper()
types = const.AppType.type_category_mapper().keys() types = const.AppType.type_category_mapper().keys()
for tp in types: for tp in types:
if not settings.XPACK_ENABLED and const.AppType.is_xpack(tp): if not settings.XPACK_ENABLED and const.AppType.is_xpack(tp):
continue continue
@ -142,7 +145,6 @@ class ApplicationTreeNodeMixin:
pid, counts, show_empty=show_empty, pid, counts, show_empty=show_empty,
show_count=show_count show_count=show_count
) )
return tree_nodes return tree_nodes
@classmethod @classmethod
@ -171,13 +173,18 @@ class ApplicationTreeNodeMixin:
pid = self.create_tree_id(pid, 'type', self.type) pid = self.create_tree_id(pid, 'type', self.type)
return pid return pid
def as_tree_node(self, pid, is_luna=False): def as_tree_node(self, pid, k8s_as_tree=False):
if is_luna and self.type == const.AppType.k8s: if self.type == const.AppType.k8s and k8s_as_tree:
node = KubernetesTree(pid).as_tree_node(self) node = KubernetesTree(pid).as_tree_node(self)
else: else:
node = self._as_tree_node(pid) node = self._as_tree_node(pid)
return node return node
def _attrs_to_tree(self):
if self.category == const.AppCategory.db:
return self.attrs
return {}
def _as_tree_node(self, pid): def _as_tree_node(self, pid):
icon_skin_category_mapper = { icon_skin_category_mapper = {
'remote_app': 'chrome', 'remote_app': 'chrome',
@ -199,6 +206,7 @@ class ApplicationTreeNodeMixin:
'data': { 'data': {
'category': self.category, 'category': self.category,
'type': self.type, 'type': self.type,
'attrs': self._attrs_to_tree()
} }
} }
}) })
@ -239,6 +247,14 @@ class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin):
def category_remote_app(self): def category_remote_app(self):
return self.category == const.AppCategory.remote_app.value 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): def get_rdp_remote_app_setting(self):
from applications.serializers.attrs import get_serializer_class_by_application_type from applications.serializers.attrs import get_serializer_class_by_application_type
if not self.category_remote_app: if not self.category_remote_app:
@ -264,12 +280,24 @@ class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin):
'parameters': parameters 'parameters': parameters
} }
def get_remote_app_asset(self): def get_remote_app_asset(self, raise_exception=True):
asset_id = self.attrs.get('asset') 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") 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): class ApplicationUser(SystemUser):

View File

@ -16,7 +16,7 @@ from perms.filters import AssetPermissionFilter
from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins.api import OrgBulkModelViewSet
from orgs.mixins import generics from orgs.mixins import generics
from assets.api import FilterAssetByNodeMixin from assets.api import FilterAssetByNodeMixin
from ..models import Asset, Node, Platform from ..models import Asset, Node, Platform, Gateway
from .. import serializers from .. import serializers
from ..tasks import ( from ..tasks import (
update_assets_hardware_info_manual, test_assets_connectivity_manual, update_assets_hardware_info_manual, test_assets_connectivity_manual,
@ -181,7 +181,7 @@ class AssetsTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
def check_permissions(self, request): def check_permissions(self, request):
action = request.data.get('action') action = request.data.get('action')
action_perm_require = { action_perm_require = {
'refresh': 'assets.refresh_assethardwareinfo1', 'refresh': 'assets.refresh_assethardwareinfo',
} }
perm_required = action_perm_require.get(action) perm_required = action_perm_require.get(action)
has = self.request.user.has_perm(perm_required) has = self.request.user.has_perm(perm_required)
@ -199,7 +199,7 @@ class AssetGatewayListApi(generics.ListAPIView):
asset_id = self.kwargs.get('pk') asset_id = self.kwargs.get('pk')
asset = get_object_or_404(Asset, pk=asset_id) asset = get_object_or_404(Asset, pk=asset_id)
if not asset.domain: if not asset.domain:
return [] return Gateway.objects.none()
queryset = asset.domain.gateways.filter(protocol='ssh') queryset = asset.domain.gateways.filter(protocol='ssh')
return queryset return queryset

View File

@ -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'),
),
]

View File

@ -223,7 +223,7 @@ class Asset(AbsConnectivity, AbsHardwareInfo, ProtocolsMixin, NodesRelationMixin
# Some information # Some information
public_ip = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Public IP')) 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")) labels = models.ManyToManyField('assets.Label', blank=True, related_name='assets', verbose_name=_("Labels"))
created_by = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Created by')) created_by = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Created by'))
@ -235,6 +235,9 @@ class Asset(AbsConnectivity, AbsHardwareInfo, ProtocolsMixin, NodesRelationMixin
def __str__(self): def __str__(self):
return '{0.hostname}({0.ip})'.format(self) return '{0.hostname}({0.ip})'.format(self)
def get_target_ip(self):
return self.ip
def set_admin_user_relation(self): def set_admin_user_relation(self):
from .authbook import AuthBook from .authbook import AuthBook
if not self.admin_user: if not self.admin_user:
@ -280,16 +283,44 @@ class Asset(AbsConnectivity, AbsHardwareInfo, ProtocolsMixin, NodesRelationMixin
def is_support_ansible(self): def is_support_ansible(self):
return self.has_protocol('ssh') and self.platform_base not in ("Other",) 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: if not self.admin_user:
return {} 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 = { info = {
'username': self.admin_user.username, 'username': auth_user.username,
'password': self.admin_user.password, 'password': auth_user.password,
'private_key': self.admin_user.private_key_file, '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 return info
def nodes_display(self): def nodes_display(self):

View File

@ -133,6 +133,14 @@ class AuthMixin:
self.password = password self.password = password
def load_app_more_auth(self, app_id=None, username=None, user_id=None): 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() self._clean_auth_info_if_manual_login_mode()
# 加载临时认证信息 # 加载临时认证信息
if self.login_mode == self.LOGIN_MANUAL: if self.login_mode == self.LOGIN_MANUAL:
@ -148,6 +156,11 @@ class AuthMixin:
_username = username _username = username
self.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=''): def load_asset_special_auth(self, asset, username=''):
""" """
AuthBook 的数据状态 AuthBook 的数据状态

View File

@ -15,6 +15,7 @@ class AdminUserSerializer(SuS):
SuS.Meta.fields_m2m + \ SuS.Meta.fields_m2m + \
[ [
'type', 'protocol', "priority", 'sftp_root', 'ssh_key_fingerprint', 'type', 'protocol', "priority", 'sftp_root', 'ssh_key_fingerprint',
'su_enabled', 'su_from',
'date_created', 'date_updated', 'comment', 'created_by', 'date_created', 'date_updated', 'comment', 'created_by',
] ]

View File

@ -43,7 +43,7 @@ class DomainSerializer(BulkOrgResourceModelSerializer):
class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
is_connective = serializers.BooleanField(required=False) is_connective = serializers.BooleanField(required=False, label=_('Connectivity'))
class Meta: class Meta:
model = Gateway model = Gateway

View File

@ -70,6 +70,7 @@ class AuthBackendLabelMapping(LazyObject):
backend_label_mapping[settings.AUTH_BACKEND_AUTH_TOKEN] = _('Auth Token') backend_label_mapping[settings.AUTH_BACKEND_AUTH_TOKEN] = _('Auth Token')
backend_label_mapping[settings.AUTH_BACKEND_WECOM] = _('WeCom') backend_label_mapping[settings.AUTH_BACKEND_WECOM] = _('WeCom')
backend_label_mapping[settings.AUTH_BACKEND_DINGTALK] = _('DingTalk') backend_label_mapping[settings.AUTH_BACKEND_DINGTALK] = _('DingTalk')
backend_label_mapping[settings.AUTH_BACKEND_TEMP_TOKEN] = _('Temporary token')
return backend_label_mapping return backend_label_mapping
def _setup(self): def _setup(self):

View File

@ -11,3 +11,4 @@ from .wecom import *
from .dingtalk import * from .dingtalk import *
from .feishu import * from .feishu import *
from .password import * from .password import *
from .temp_token import *

View File

@ -8,7 +8,6 @@ from .. import serializers
class AccessKeyViewSet(ModelViewSet): class AccessKeyViewSet(ModelViewSet):
permission_classes = (IsValidUser,)
serializer_class = serializers.AccessKeySerializer serializer_class = serializers.AccessKeySerializer
search_fields = ['^id', '^secret'] search_fields = ['^id', '^secret']

View File

@ -30,13 +30,14 @@ from common.http import is_true
from perms.models.base import Action from perms.models.base import Action
from perms.utils.application.permission import get_application_actions from perms.utils.application.permission import get_application_actions
from perms.utils.asset.permission import get_asset_actions from perms.utils.asset.permission import get_asset_actions
from common.const.http import PATCH
from terminal.models import EndpointRule
from ..serializers import ( from ..serializers import (
ConnectionTokenSerializer, ConnectionTokenSecretSerializer, ConnectionTokenSerializer, ConnectionTokenSecretSerializer,
) )
logger = get_logger(__name__) logger = get_logger(__name__)
__all__ = ['UserConnectionTokenViewSet'] __all__ = ['UserConnectionTokenViewSet', 'TokenCacheMixin']
class ClientProtocolMixin: class ClientProtocolMixin:
@ -51,6 +52,17 @@ class ClientProtocolMixin:
request: Request request: Request
get_serializer: Callable get_serializer: Callable
create_token: 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): def get_request_resource(self, serializer):
asset = serializer.validated_data.get('asset') asset = serializer.validated_data.get('asset')
@ -92,7 +104,7 @@ class ClientProtocolMixin:
'autoreconnection enabled:i': '1', 'autoreconnection enabled:i': '1',
'bookmarktype:i': '3', 'bookmarktype:i': '3',
'use redirection server name:i': '0', 'use redirection server name:i': '0',
'smart sizing:i': '0', 'smart sizing:i': '1',
#'drivestoredirect:s': '*', #'drivestoredirect:s': '*',
# 'domain:s': '' # 'domain:s': ''
# 'alternate shell:s:': '||MySQLWorkbench', # 'alternate shell:s:': '||MySQLWorkbench',
@ -122,10 +134,10 @@ class ClientProtocolMixin:
options['screen mode id:i'] = '2' if full_screen else '1' options['screen mode id:i'] = '2' if full_screen else '1'
# RDP Server 地址 # RDP Server 地址
address = settings.TERMINAL_RDP_ADDR endpoint = self.get_smart_endpoint(
if not address or address == 'localhost:3389': protocol='rdp', asset=asset, application=application
address = self.request.get_host().split(':')[0] + ':3389' )
options['full address:s'] = address options['full address:s'] = f'{endpoint.host}:{endpoint.rdp_port}'
# 用户名 # 用户名
options['username:s'] = '{}|{}'.format(user.username, token) options['username:s'] = '{}|{}'.format(user.username, token)
if system_user.ad_domain: if system_user.ad_domain:
@ -134,8 +146,7 @@ class ClientProtocolMixin:
if width and height: if width and height:
options['desktopwidth:i'] = width options['desktopwidth:i'] = width
options['desktopheight:i'] = height options['desktopheight:i'] = height
else: options['winposstr:s:'] = f'0,1,0,0,{width},{height}'
options['smart sizing:i'] = '1'
options['session bpp:i'] = os.getenv('JUMPSERVER_COLOR_DEPTH', '32') options['session bpp:i'] = os.getenv('JUMPSERVER_COLOR_DEPTH', '32')
options['audiomode:i'] = self.parse_env_bool('JUMPSERVER_DISABLE_AUDIO', 'false', '2', '0') options['audiomode:i'] = self.parse_env_bool('JUMPSERVER_DISABLE_AUDIO', 'false', '2', '0')
@ -160,6 +171,28 @@ class ClientProtocolMixin:
content += f'{k}:{v}\n' content += f'{k}:{v}\n'
return name, content 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): def get_encrypt_cmdline(self, app: Application):
parameters = app.get_rdp_remote_app_setting()['parameters'] parameters = app.get_rdp_remote_app_setting()['parameters']
parameters = parameters.encode('ascii') parameters = parameters.encode('ascii')
@ -201,13 +234,11 @@ class ClientProtocolMixin:
asset, application, system_user, user = self.get_request_resource(serializer) asset, application, system_user, user = self.get_request_resource(serializer)
protocol = system_user.protocol protocol = system_user.protocol
username = user.username username = user.username
config, token = '', ''
if protocol == 'rdp': if protocol == 'rdp':
name, config = self.get_rdp_file_content(serializer) name, config = self.get_rdp_file_content(serializer)
elif protocol == 'ssh': elif protocol == 'ssh':
# Todo: name, token = self.get_ssh_token(serializer)
name = ''
config = 'ssh://system_user@asset@user@jumpserver-ssh'
else: else:
raise ValueError('Protocol not support: {}'.format(protocol)) raise ValueError('Protocol not support: {}'.format(protocol))
@ -216,6 +247,7 @@ class ClientProtocolMixin:
"filename": filename, "filename": filename,
"protocol": system_user.protocol, "protocol": system_user.protocol,
"username": username, "username": username,
"token": token,
"config": config "config": config
} }
return data return data
@ -327,18 +359,56 @@ class SecretDetailMixin:
return Response(data=serializer.data, status=200) 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( class UserConnectionTokenViewSet(
RootOrgViewMixin, SerializerMixin, ClientProtocolMixin, RootOrgViewMixin, SerializerMixin, ClientProtocolMixin,
SecretDetailMixin, GenericViewSet SecretDetailMixin, TokenCacheMixin, GenericViewSet
): ):
serializer_classes = { serializer_classes = {
'default': ConnectionTokenSerializer, 'default': ConnectionTokenSerializer,
'get_secret_detail': ConnectionTokenSecretSerializer, 'get_secret_detail': ConnectionTokenSecretSerializer,
} }
CACHE_KEY_PREFIX = 'CONNECTION_TOKEN_{}'
rbac_perms = { rbac_perms = {
'GET': 'authentication.view_connectiontoken', 'GET': 'authentication.view_connectiontoken',
'create': 'authentication.add_connectiontoken', 'create': 'authentication.add_connectiontoken',
'renewal': 'authentication.add_superconnectiontoken',
'get_secret_detail': 'authentication.view_connectiontokensecret', 'get_secret_detail': 'authentication.view_connectiontokensecret',
'get_rdp_file': 'authentication.add_connectiontoken', 'get_rdp_file': 'authentication.add_connectiontoken',
'get_client_protocol_url': 'authentication.add_connectiontoken', 'get_client_protocol_url': 'authentication.add_connectiontoken',
@ -359,6 +429,17 @@ class UserConnectionTokenViewSet(
raise PermissionDenied(error) raise PermissionDenied(error)
return True 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): def create_token(self, user, asset, application, system_user, ttl=5*60):
# 再次强调一下权限 # 再次强调一下权限
perm_required = 'authentication.add_superconnectiontoken' perm_required = 'authentication.add_superconnectiontoken'
@ -391,8 +472,7 @@ class UserConnectionTokenViewSet(
'application_name': str(application) 'application_name': str(application)
}) })
key = self.CACHE_KEY_PREFIX.format(token) self.set_token_to_cache(token, value, ttl)
cache.set(key, value, timeout=ttl)
return token, secret return token, secret
def create(self, request, *args, **kwargs): 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.asset.permission import validate_permission as asset_validate_permission
from perms.utils.application.permission import validate_permission as app_validate_permission from perms.utils.application.permission import validate_permission as app_validate_permission
key = self.CACHE_KEY_PREFIX.format(token) value = self.get_token_from_cache(token)
value = cache.get(key, None)
if not value: if not value:
raise serializers.ValidationError('Token not found') raise serializers.ValidationError('Token not found')
@ -442,9 +521,7 @@ class UserConnectionTokenViewSet(
def get(self, request): def get(self, request):
token = request.query_params.get('token') token = request.query_params.get('token')
key = self.CACHE_KEY_PREFIX.format(token) value = self.get_token_from_cache(token)
value = cache.get(key, None)
if not value: if not value:
return Response('', status=404) return Response('', status=404)
return Response(value) return Response(value)

View File

@ -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)

View File

@ -1,6 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.shortcuts import redirect
from rest_framework.permissions import AllowAny from rest_framework.permissions import AllowAny
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.generics import CreateAPIView from rest_framework.generics import CreateAPIView

View File

@ -1,10 +1,11 @@
from django.contrib.auth.backends import BaseBackend
from django.contrib.auth.backends import ModelBackend from django.contrib.auth.backends import ModelBackend
from django.contrib.auth import get_user_model
from users.models import User from users.models import User
from common.utils import get_logger from common.utils import get_logger
UserModel = get_user_model()
logger = get_logger(__file__) logger = get_logger(__file__)

View File

@ -53,7 +53,7 @@ class LDAPAuthorizationBackend(JMSBaseAuthBackend, LDAPBackend):
else: else:
built = False built = False
return (user, built) return user, built
def pre_check(self, username, password): def pre_check(self, username, password):
if not settings.AUTH_LDAP: if not settings.AUTH_LDAP:
@ -75,6 +75,9 @@ class LDAPAuthorizationBackend(JMSBaseAuthBackend, LDAPBackend):
def authenticate(self, request=None, username=None, password=None, **kwargs): def authenticate(self, request=None, username=None, password=None, **kwargs):
logger.info('Authentication LDAP backend') 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) match, msg = self.pre_check(username, password)
if not match: if not match:
logger.info('Authenticate failed: {}'.format(msg)) logger.info('Authenticate failed: {}'.format(msg))

View File

@ -13,17 +13,20 @@ User = get_user_model()
class CreateUserMixin: 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): if isinstance(username, bytes):
username = username.decode() username = username.decode()
try: user = User.objects.filter(username=username).first()
user = User.objects.get(username=username) if user:
except User.DoesNotExist: return user
if '@' in username: if '@' in username:
email = username email = username
else: else:
email_suffix = settings.EMAIL_SUFFIX email_suffix = settings.EMAIL_SUFFIX
email = '{}@{}'.format(username, email_suffix) email = '{}@{}'.format(username, email_suffix)
user = User(username=username, name=username, email=email) user = User(username=username, name=username, email=email)
user.source = user.Source.radius.value user.source = user.Source.radius.value
user.save() user.save()

View File

@ -14,7 +14,7 @@ from ..base import JMSModelBackend
__all__ = ['SAML2Backend'] __all__ = ['SAML2Backend']
logger = get_logger(__file__) logger = get_logger(__name__)
class SAML2Backend(JMSModelBackend): class SAML2Backend(JMSModelBackend):

View File

@ -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

View File

@ -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',
},
),
]

View File

@ -1,8 +1,9 @@
import uuid import uuid
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework.authtoken.models import Token
from django.conf import settings from django.conf import settings
from rest_framework.authtoken.models import Token
from common.db import models 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 SuperConnectionToken(ConnectionToken):
class Meta: class Meta:
proxy = True proxy = True

View File

@ -0,0 +1,3 @@
from .token import *
from .connect_token import *
from .password_mfa import *

View File

@ -1,109 +1,22 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.utils import timezone
from rest_framework import serializers from rest_framework import serializers
from common.utils import get_object_or_none
from users.models import User from users.models import User
from assets.models import Asset, SystemUser, Gateway, Domain, CommandFilterRule from assets.models import Asset, SystemUser, Gateway, Domain, CommandFilterRule
from applications.models import Application from applications.models import Application
from users.serializers import UserProfileSerializer
from assets.serializers import ProtocolsField from assets.serializers import ProtocolsField
from perms.serializers.base import ActionsField from perms.serializers.base import ActionsField
from .models import AccessKey
__all__ = [ __all__ = [
'AccessKeySerializer', 'OtpVerifySerializer', 'BearerTokenSerializer', 'ConnectionTokenSerializer', 'ConnectionTokenApplicationSerializer',
'MFAChallengeSerializer', 'SSOTokenSerializer', 'ConnectionTokenUserSerializer', 'ConnectionTokenFilterRuleSerializer',
'ConnectionTokenSerializer', 'ConnectionTokenSecretSerializer', 'ConnectionTokenAssetSerializer', 'ConnectionTokenSystemUserSerializer',
'PasswordVerifySerializer', 'MFASelectTypeSerializer', '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): class ConnectionTokenSerializer(serializers.Serializer):
user = serializers.CharField(max_length=128, required=False, allow_blank=True) user = serializers.CharField(max_length=128, required=False, allow_blank=True)
system_user = serializers.CharField(max_length=128, required=True) system_user = serializers.CharField(max_length=128, required=True)

View File

@ -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)

View File

@ -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

View File

@ -142,15 +142,18 @@
<li class="dropdown"> <li class="dropdown">
<a class="dropdown-toggle login-page-language" data-toggle="dropdown" href="#" target="_blank"> <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> <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> <span>English<b class="caret"></b></span>
{% elif request.COOKIES.django_language == 'ja' %}
<span>日本語<b class="caret"></b></span>
{% else %} {% else %}
<span>中文(简体)<b class="caret"></b></span> <span>中文(简体)<b class="caret"></b></span>
{% endifequal %} {% endif %}
</a> </a>
<ul class="dropdown-menu profile-dropdown dropdown-menu-right"> <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_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_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> </ul>
</li> </li>
</ul> </ul>

View File

@ -9,6 +9,7 @@ app_name = 'authentication'
router = DefaultRouter() router = DefaultRouter()
router.register('access-keys', api.AccessKeyViewSet, 'access-key') router.register('access-keys', api.AccessKeyViewSet, 'access-key')
router.register('sso', api.SSOViewSet, 'sso') router.register('sso', api.SSOViewSet, 'sso')
router.register('temp-tokens', api.TempTokenViewSet, 'temp-token')
router.register('connection-token', api.UserConnectionTokenViewSet, 'connection-token') router.register('connection-token', api.UserConnectionTokenViewSet, 'connection-token')

View File

@ -27,12 +27,16 @@ urlpatterns = [
path('wecom/qr/login/', views.WeComQRLoginView.as_view(), name='wecom-qr-login'), 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/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/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/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/bind/', views.DingTalkQRBindView.as_view(), name='dingtalk-qr-bind'),
path('dingtalk/qr/login/', views.DingTalkQRLoginView.as_view(), name='dingtalk-qr-login'), 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/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/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/bind/start/', views.FeiShuEnableStartView.as_view(), name='feishu-bind-start'),
path('feishu/qr/bind/', views.FeiShuQRBindView.as_view(), name='feishu-qr-bind'), 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/enable/bind/', users_view.UserOtpEnableBindView.as_view(), name='user-otp-enable-bind'),
path('profile/otp/disable/', users_view.UserOtpDisableView.as_view(), path('profile/otp/disable/', users_view.UserOtpDisableView.as_view(),
name='user-otp-disable'), name='user-otp-disable'),
path('first-login/', users_view.UserFirstLoginView.as_view(), name='user-first-login'),
# openid # openid
path('cas/', include(('authentication.backends.cas.urls', 'authentication'), namespace='cas')), path('cas/', include(('authentication.backends.cas.urls', 'authentication'), namespace='cas')),

View File

@ -9,8 +9,7 @@ from django.conf import settings
from .notifications import DifferentCityLoginMessage from .notifications import DifferentCityLoginMessage
from audits.models import UserLoginLog from audits.models import UserLoginLog
from audits.const import DEFAULT_CITY from audits.const import DEFAULT_CITY
from common.utils import get_request_ip from common.utils import validate_ip, get_ip_city, get_request_ip
from common.utils import validate_ip, get_ip_city
from common.utils import get_logger from common.utils import get_logger
logger = get_logger(__file__) logger = get_logger(__file__)

View File

@ -28,7 +28,7 @@ logger = get_logger(__file__)
DINGTALK_STATE_SESSION_KEY = '_dingtalk_state' DINGTALK_STATE_SESSION_KEY = '_dingtalk_state'
class DingTalkQRMixin(PermissionsMixin, View): class DingTalkBaseMixin(PermissionsMixin, View):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
try: try:
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
@ -54,20 +54,6 @@ class DingTalkQRMixin(PermissionsMixin, View):
msg = _("The system configuration is incorrect. Please contact your administrator") msg = _("The system configuration is incorrect. Please contact your administrator")
return self.get_failed_response(redirect_uri, msg, msg) 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 @staticmethod
def get_success_response(redirect_url, title, msg): def get_success_response(redirect_url, title, msg):
message_data = { message_data = {
@ -94,6 +80,42 @@ class DingTalkQRMixin(PermissionsMixin, View):
return response 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): class DingTalkQRBindView(DingTalkQRMixin, View):
permission_classes = (IsAuthenticated,) permission_classes = (IsAuthenticated,)
@ -230,3 +252,57 @@ class DingTalkQRLoginCallbackView(AuthMixin, DingTalkQRMixin, View):
return response return response
return self.redirect_to_guard_view() 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()

View File

@ -28,7 +28,7 @@ logger = get_logger(__file__)
WECOM_STATE_SESSION_KEY = '_wecom_state' WECOM_STATE_SESSION_KEY = '_wecom_state'
class WeComQRMixin(PermissionsMixin, View): class WeComBaseMixin(PermissionsMixin, View):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
try: try:
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
@ -54,19 +54,6 @@ class WeComQRMixin(PermissionsMixin, View):
msg = _("The system configuration is incorrect. Please contact your administrator") msg = _("The system configuration is incorrect. Please contact your administrator")
return self.get_failed_response(redirect_uri, msg, msg) 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 @staticmethod
def get_success_response(redirect_url, title, msg): def get_success_response(redirect_url, title, msg):
message_data = { message_data = {
@ -93,6 +80,42 @@ class WeComQRMixin(PermissionsMixin, View):
return response 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): class WeComQRBindView(WeComQRMixin, View):
permission_classes = (IsAuthenticated,) permission_classes = (IsAuthenticated,)
@ -225,3 +248,57 @@ class WeComQRLoginCallbackView(AuthMixin, WeComQRMixin, View):
return response return response
return self.redirect_to_guard_view() 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()

View File

@ -1,11 +1,9 @@
import time import time
from redis import Redis
from common.utils.lock import DistributedLock from common.utils.lock import DistributedLock
from common.utils.connection import get_redis_client
from common.utils import lazyproperty from common.utils import lazyproperty
from common.utils import get_logger from common.utils import get_logger
from jumpserver.const import CONFIG
logger = get_logger(__file__) logger = get_logger(__file__)
@ -58,7 +56,7 @@ class Cache(metaclass=CacheType):
def __init__(self): def __init__(self):
self._data = None 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): def __getitem__(self, item):
return self.field_desc_mapper[item] return self.field_desc_mapper[item]

View File

@ -4,7 +4,7 @@ import json
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.core.validators import MinValueValidator, MaxValueValidator
from ..utils import signer, crypto from ..utils import signer, crypto
@ -13,7 +13,7 @@ __all__ = [
'JsonCharField', 'JsonTextField', 'JsonListCharField', 'JsonListTextField', 'JsonCharField', 'JsonTextField', 'JsonListCharField', 'JsonListTextField',
'JsonDictCharField', 'JsonDictTextField', 'EncryptCharField', 'JsonDictCharField', 'JsonDictTextField', 'EncryptCharField',
'EncryptTextField', 'EncryptMixin', 'EncryptJsonDictTextField', 'EncryptTextField', 'EncryptMixin', 'EncryptJsonDictTextField',
'EncryptJsonDictCharField', 'EncryptJsonDictCharField', 'PortField'
] ]
@ -180,3 +180,13 @@ class EncryptJsonDictTextField(EncryptMixin, JsonDictTextField):
class EncryptJsonDictCharField(EncryptMixin, JsonDictCharField): class EncryptJsonDictCharField(EncryptMixin, JsonDictCharField):
pass 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)

View File

@ -28,6 +28,7 @@ class ErrorCode:
class URL: class URL:
QR_CONNECT = 'https://oapi.dingtalk.com/connect/qrconnect' 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_USER_INFO_BY_CODE = 'https://oapi.dingtalk.com/sns/getuserinfo_bycode'
GET_TOKEN = 'https://oapi.dingtalk.com/gettoken' GET_TOKEN = 'https://oapi.dingtalk.com/gettoken'
SEND_MESSAGE_BY_TEMPLATE = 'https://oapi.dingtalk.com/topapi/message/corpconversation/sendbytemplate' SEND_MESSAGE_BY_TEMPLATE = 'https://oapi.dingtalk.com/topapi/message/corpconversation/sendbytemplate'

View File

@ -19,6 +19,7 @@ class URL:
GET_TOKEN = 'https://qyapi.weixin.qq.com/cgi-bin/gettoken' GET_TOKEN = 'https://qyapi.weixin.qq.com/cgi-bin/gettoken'
SEND_MESSAGE = 'https://qyapi.weixin.qq.com/cgi-bin/message/send' SEND_MESSAGE = 'https://qyapi.weixin.qq.com/cgi-bin/message/send'
QR_CONNECT = 'https://open.work.weixin.qq.com/wwopen/sso/qrConnect' 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 # 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' GET_USER_ID_BY_CODE = 'https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo'

View File

@ -31,6 +31,8 @@ def combine_seq(s1, s2, callback=None):
def get_logger(name=''): def get_logger(name=''):
if '/' in name:
name = os.path.basename(name).replace('.py', '')
return logging.getLogger('jumpserver.%s' % name) 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 settings.BASE_DIR, dir, platform_name, arch, filename
) )
return file_path 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

View File

@ -1,23 +1,30 @@
import json import json
import threading import threading
import redis from redis import Redis
from django.conf import settings 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.db.utils import safe_db_connection
from common.utils import get_logger from common.utils import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
def get_redis_client(db): def get_redis_client(db=0):
rc = redis.StrictRedis( params = {
host=settings.REDIS_HOST, 'host': CONFIG.REDIS_HOST,
port=settings.REDIS_PORT, 'port': CONFIG.REDIS_PORT,
password=settings.REDIS_PASSWORD, 'password': CONFIG.REDIS_PASSWORD,
db=db 'db': db,
) "ssl": is_true(CONFIG.REDIS_USE_SSL),
return rc '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: class Subscription:
@ -99,5 +106,3 @@ class RedisPubSub:
data_json = json.dumps(data) data_json = json.dumps(data)
self.redis.publish(self.ch, data_json) self.redis.publish(self.ch, data_json)
return True return True

View File

@ -1,9 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import re import re
from django.shortcuts import reverse as dj_reverse from django.shortcuts import reverse as dj_reverse
from django.conf import settings from django.conf import settings
from django.utils import timezone 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}') UUID_PATTERN = re.compile(r'[0-9a-zA-Z\-]{36}')
@ -58,3 +61,12 @@ def get_log_keep_day(s, defaults=200):
except ValueError: except ValueError:
days = defaults days = defaults
return days 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

View File

@ -10,6 +10,7 @@ from django.db import transaction
from common.utils import get_logger from common.utils import get_logger
from common.utils.inspect import copy_function_args from common.utils.inspect import copy_function_args
from common.utils.connection import get_redis_client
from jumpserver.const import CONFIG from jumpserver.const import CONFIG
from common.local import thread_local from common.local import thread_local
@ -44,7 +45,7 @@ class DistributedLock(RedisLock):
是否可重入 是否可重入
""" """
self.kwargs_copy = copy_function_args(self.__init__, locals()) 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: if expire is None:
expire = auto_renewal_seconds expire = auto_renewal_seconds

View File

@ -40,27 +40,3 @@ def random_string(length, lower=True, upper=True, digit=True, special_char=False
password = ''.join(password) password = ''.join(password)
return 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)]

View File

@ -300,32 +300,6 @@ class IndexApi(DatesLoginMetricMixin, APIView):
class HealthApiMixin(APIView): class HealthApiMixin(APIView):
pass 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): class HealthCheckView(HealthApiMixin):
permission_classes = (AllowAny,) permission_classes = (AllowAny,)

View File

@ -16,8 +16,10 @@ import json
import yaml import yaml
import copy import copy
from importlib import import_module from importlib import import_module
from django.urls import reverse_lazy
from urllib.parse import urljoin, urlparse 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 _ from django.utils.translation import ugettext_lazy as _
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 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_IS_PERIODIC': False,
'AUTH_LDAP_SYNC_INTERVAL': None, 'AUTH_LDAP_SYNC_INTERVAL': None,
'AUTH_LDAP_SYNC_CRONTAB': 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_USER_LOGIN_ONLY_IN_USERS': False,
'AUTH_LDAP_OPTIONS_OPT_REFERRALS': -1, 'AUTH_LDAP_OPTIONS_OPT_REFERRALS': -1,
@ -253,6 +256,8 @@ class Config(dict):
'AUTH_SAML2_PROVIDER_AUTHORIZATION_ENDPOINT': '/', 'AUTH_SAML2_PROVIDER_AUTHORIZATION_ENDPOINT': '/',
'AUTH_SAML2_AUTHENTICATION_FAILURE_REDIRECT_URI': '/', 'AUTH_SAML2_AUTHENTICATION_FAILURE_REDIRECT_URI': '/',
'AUTH_TEMP_TOKEN': False,
# 企业微信 # 企业微信
'AUTH_WECOM': False, 'AUTH_WECOM': False,
'WECOM_CORPID': '', 'WECOM_CORPID': '',
@ -307,7 +312,11 @@ class Config(dict):
'TERMINAL_HOST_KEY': '', 'TERMINAL_HOST_KEY': '',
'TERMINAL_TELNET_REGEX': '', 'TERMINAL_TELNET_REGEX': '',
'TERMINAL_COMMAND_STORAGE': {}, 'TERMINAL_COMMAND_STORAGE': {},
# 未来废弃(当下迁移会用)
'TERMINAL_RDP_ADDR': '', 'TERMINAL_RDP_ADDR': '',
# 保留(Luna还在用)
'TERMINAL_MAGNUS_ENABLED': True,
# 保留(Luna还在用)
'XRDP_ENABLED': True, 'XRDP_ENABLED': True,
# 安全配置 # 安全配置
@ -392,6 +401,7 @@ class Config(dict):
'FORGOT_PASSWORD_URL': '', 'FORGOT_PASSWORD_URL': '',
'HEALTH_CHECK_TOKEN': '', 'HEALTH_CHECK_TOKEN': '',
} }
@staticmethod @staticmethod
@ -540,7 +550,8 @@ class Config(dict):
value = self.get_from_env(item) value = self.get_from_env(item)
if value is not None: if value is not None:
return value return value
return self.defaults.get(item) value = self.defaults.get(item)
return value
def __getitem__(self, item): def __getitem__(self, item):
return self.get(item) return self.get(item)

View File

@ -1,8 +1,40 @@
from redis_sessions.session import force_unicode, SessionStore as RedisSessionStore from redis_sessions.session import (
from redis import exceptions 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): class SessionStore(RedisSessionStore):
def __init__(self, session_key=None):
super(SessionStore, self).__init__(session_key)
self.server = RedisServer(session_key).get()
def load(self): def load(self):
try: try:

View File

@ -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_IS_PERIODIC = CONFIG.AUTH_LDAP_SYNC_IS_PERIODIC
AUTH_LDAP_SYNC_INTERVAL = CONFIG.AUTH_LDAP_SYNC_INTERVAL AUTH_LDAP_SYNC_INTERVAL = CONFIG.AUTH_LDAP_SYNC_INTERVAL
AUTH_LDAP_SYNC_CRONTAB = CONFIG.AUTH_LDAP_SYNC_CRONTAB 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 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_RENAME_ATTRIBUTES = CONFIG.CAS_RENAME_ATTRIBUTES
CAS_CREATE_USER = CONFIG.CAS_CREATE_USER CAS_CREATE_USER = CONFIG.CAS_CREATE_USER
# SSO Auth # SSO auth
AUTH_SSO = CONFIG.AUTH_SSO AUTH_SSO = CONFIG.AUTH_SSO
AUTH_SSO_AUTHKEY_TTL = CONFIG.AUTH_SSO_AUTHKEY_TTL AUTH_SSO_AUTHKEY_TTL = CONFIG.AUTH_SSO_AUTHKEY_TTL
# WECOM Auth # WECOM auth
AUTH_WECOM = CONFIG.AUTH_WECOM AUTH_WECOM = CONFIG.AUTH_WECOM
WECOM_CORPID = CONFIG.WECOM_CORPID WECOM_CORPID = CONFIG.WECOM_CORPID
WECOM_AGENTID = CONFIG.WECOM_AGENTID 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_LOGIN_URL_NAME = "authentication:saml2:saml2-login"
SAML2_LOGOUT_URL_NAME = "authentication:saml2:saml2-logout" SAML2_LOGOUT_URL_NAME = "authentication:saml2:saml2-logout"
# 临时 token
AUTH_TEMP_TOKEN = CONFIG.AUTH_TEMP_TOKEN
# Other setting # Other setting
TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION
OTP_IN_RADIUS = CONFIG.OTP_IN_RADIUS 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_FEISHU = 'authentication.backends.sso.FeiShuAuthentication'
AUTH_BACKEND_AUTH_TOKEN = 'authentication.backends.sso.AuthorizationTokenAuthentication' AUTH_BACKEND_AUTH_TOKEN = 'authentication.backends.sso.AuthorizationTokenAuthentication'
AUTH_BACKEND_SAML2 = 'authentication.backends.saml2.SAML2Backend' AUTH_BACKEND_SAML2 = 'authentication.backends.saml2.SAML2Backend'
AUTH_BACKEND_TEMP_TOKEN = 'authentication.backends.token.TempTokenAuthBackend'
AUTHENTICATION_BACKENDS = [ AUTHENTICATION_BACKENDS = [
@ -171,7 +176,7 @@ AUTHENTICATION_BACKENDS = [
# 扫码模式 # 扫码模式
AUTH_BACKEND_WECOM, AUTH_BACKEND_DINGTALK, AUTH_BACKEND_FEISHU, AUTH_BACKEND_WECOM, AUTH_BACKEND_DINGTALK, AUTH_BACKEND_FEISHU,
# Token模式 # 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 ONLY_ALLOW_EXIST_USER_AUTH = CONFIG.ONLY_ALLOW_EXIST_USER_AUTH

View File

@ -127,7 +127,7 @@ LOGIN_REDIRECT_URL = reverse_lazy('index')
LOGIN_URL = reverse_lazy('authentication:login') LOGIN_URL = reverse_lazy('authentication:login')
SESSION_COOKIE_DOMAIN = CONFIG.SESSION_COOKIE_DOMAIN 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_COOKIE_AGE = CONFIG.SESSION_COOKIE_AGE
SESSION_EXPIRE_AT_BROWSER_CLOSE = True SESSION_EXPIRE_AT_BROWSER_CLOSE = True
# 自定义的配置SESSION_EXPIRE_AT_BROWSER_CLOSE 始终为 True, 下面这个来控制是否强制关闭后过期 cookie # 自定义的配置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_SAVE_EVERY_REQUEST = CONFIG.SESSION_SAVE_EVERY_REQUEST
SESSION_ENGINE = 'jumpserver.rewriting.session' SESSION_ENGINE = 'jumpserver.rewriting.session'
SESSION_REDIS = { 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, 'host': CONFIG.REDIS_HOST,
'port': CONFIG.REDIS_PORT, 'port': CONFIG.REDIS_PORT,
'password': CONFIG.REDIS_PASSWORD, 'db': CONFIG.REDIS_DB_CACHE,
'db': CONFIG.REDIS_DB_SESSION, },
'prefix': 'auth_session', 'prefix': 'auth_session',
'socket_timeout': 1, 'socket_timeout': 1,
'retry_on_timeout': False 'retry_on_timeout': False
@ -246,18 +249,37 @@ FILE_UPLOAD_PERMISSIONS = 0o644
FILE_UPLOAD_DIRECTORY_PERMISSIONS = 0o755 FILE_UPLOAD_DIRECTORY_PERMISSIONS = 0o755
# Cache use redis # 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 = { CACHES = {
'default': { 'default': {
# 'BACKEND': 'redis_cache.RedisCache', # 'BACKEND': 'redis_cache.RedisCache',
'BACKEND': 'redis_lock.django_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, 'password': CONFIG.REDIS_PASSWORD,
'host': CONFIG.REDIS_HOST, 'host': CONFIG.REDIS_HOST,
'port': CONFIG.REDIS_PORT, 'port': CONFIG.REDIS_PORT,
'db': CONFIG.REDIS_DB_CACHE, 'db': CONFIG.REDIS_DB_CACHE,
}, },
'OPTIONS': { '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 {}
} }
} }
} }

View File

@ -1,6 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import os import os
import ssl
from .base import REDIS_SSL_CA_CERTS, REDIS_SSL_CERTFILE, REDIS_SSL_KEYFILE
from ..const import CONFIG, PROJECT_DIR from ..const import CONFIG, PROJECT_DIR
REST_FRAMEWORK = { REST_FRAMEWORK = {
@ -82,16 +85,25 @@ BOOTSTRAP3 = {
# Django channels support websocket # Django channels support websocket
CHANNEL_REDIS = "redis://:{}@{}:{}/{}".format( if not CONFIG.REDIS_USE_SSL:
CONFIG.REDIS_PASSWORD, CONFIG.REDIS_HOST, CONFIG.REDIS_PORT, context = None
CONFIG.REDIS_DB_WS, 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 = { CHANNEL_LAYERS = {
'default': { 'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer', 'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': { '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_LOG_DIR = os.path.join(PROJECT_DIR, 'data', 'celery')
# Celery using redis as broker # 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, 'password': CONFIG.REDIS_PASSWORD,
'host': CONFIG.REDIS_HOST, 'host': CONFIG.REDIS_HOST,
'port': CONFIG.REDIS_PORT, 'port': CONFIG.REDIS_PORT,
@ -125,6 +138,13 @@ CELERY_WORKER_REDIRECT_STDOUTS_LEVEL = "INFO"
# CELERY_WORKER_HIJACK_ROOT_LOGGER = True # CELERY_WORKER_HIJACK_ROOT_LOGGER = True
# CELERY_WORKER_MAX_TASKS_PER_CHILD = 40 # CELERY_WORKER_MAX_TASKS_PER_CHILD = 40
CELERY_TASK_SOFT_TIME_LIMIT = 3600 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') ANSIBLE_LOG_DIR = os.path.join(PROJECT_DIR, 'data', 'ansible')

View File

@ -1,4 +1,4 @@
from django.views.generic import TemplateView from django.views.generic import View
from django.shortcuts import redirect from django.shortcuts import redirect
from common.permissions import IsValidUser from common.permissions import IsValidUser
from common.mixins.views import PermissionsMixin from common.mixins.views import PermissionsMixin
@ -6,8 +6,7 @@ from common.mixins.views import PermissionsMixin
__all__ = ['IndexView'] __all__ = ['IndexView']
class IndexView(PermissionsMixin, TemplateView): class IndexView(PermissionsMixin, View):
template_name = 'index.html'
permission_classes = [IsValidUser] permission_classes = [IsValidUser]
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):

View File

@ -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

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1c4a4fa3abb21fea213011d50fd62455fb1ddf73538401dc8cd9c03f4f4bbc77
size 3322

View File

@ -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 "更新に失敗しました"

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:449810c3661c09f6448b9c67e7a193f303a3bef7ccc3d0f1efe6e099804e782a oid sha256:c5e41035cf1525f01fb773511041f0f8a3a25cdfb1fa4f1e681c6d7eec85f6b9
size 104323 size 103570

File diff suppressed because it is too large Load Diff

View File

@ -59,7 +59,7 @@ class CommandExecutionViewSet(RootOrgViewMixin, viewsets.ModelViewSet):
raise ValidationError({"hosts": msg}) raise ValidationError({"hosts": msg})
def check_permissions(self, request): 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 self.permission_denied(request, "Command execution disabled")
return super().check_permissions(request) return super().check_permissions(request)

View File

@ -29,13 +29,7 @@ class JMSBaseInventory(BaseInventory):
if asset.domain and asset.domain.has_gateway(): if asset.domain and asset.domain.has_gateway():
info["vars"].update(self.make_proxy_command(asset)) info["vars"].update(self.make_proxy_command(asset))
if run_as_admin: if run_as_admin:
info.update(asset.get_auth_info()) info.update(asset.get_auth_info(with_become=True))
if asset.is_unixlike():
info["become"] = {
"method": 'sudo',
"user": 'root',
"pass": ''
}
if asset.is_windows(): if asset.is_windows():
info["vars"].update({ info["vars"].update({
"ansible_connection": "ssh", "ansible_connection": "ssh",

View File

@ -1,33 +1,61 @@
from functools import wraps
from django.db.models.signals import post_save, pre_delete, pre_save, post_delete from django.db.models.signals import post_save, pre_delete, pre_save, post_delete
from django.dispatch import receiver from django.dispatch import receiver
from orgs.models import Organization from orgs.models import Organization
from assets.models import Node 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.models import UserGroup, User
from users.signals import pre_user_leave_org
from applications.models import Application from applications.models import Application
from terminal.models import Session from terminal.models import Session
from rbac.models import OrgRoleBinding, SystemRoleBinding
from assets.models import Asset, SystemUser, Domain, Gateway from assets.models import Asset, SystemUser, Domain, Gateway
from orgs.caches import OrgResourceStatisticsCache 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): def refresh_cache(name, org):
orgs = Organization.objects.filter(m2m_org_members__user_id=user_id).distinct() 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: for org in orgs:
org_cache = OrgResourceStatisticsCache(org) refresh_cache('users_amount', org)
org_cache.expire('users_amount')
OrgResourceStatisticsCache(Organization.root()).expire('users_amount')
@receiver(post_save, sender=User) @receiver(post_save, sender=OrgRoleBinding)
def on_user_create_refresh_cache(sender, instance, created, **kwargs): def on_user_create_or_invite_refresh_cache(sender, instance, created, **kwargs):
if created: 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) @receiver(pre_delete, sender=User)
def on_user_delete_refresh_cache(sender, instance, **kwargs): 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) # @receiver(m2m_changed, sender=OrganizationMember)

View File

@ -22,7 +22,3 @@ class AppRoleUserMixin(_RoleUserMixin):
('get_tree', 'perms.view_myapps'), ('get_tree', 'perms.view_myapps'),
('GET', 'perms.view_myapps'), ('GET', 'perms.view_myapps'),
) )
def dispatch(self, *args, **kwargs):
with tmp_to_root_org():
return super().dispatch(*args, **kwargs)

View File

@ -1,18 +1,18 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from typing import Callable
from rest_framework.generics import ListAPIView from rest_framework.generics import ListAPIView
from rest_framework.response import Response from rest_framework.response import Response
from common.mixins.api import CommonApiMixin from common.mixins.api import CommonApiMixin
from common.tree import TreeNodeSerializer from common.tree import TreeNodeSerializer
from applications.api.mixin import (
SerializeApplicationToTreeNodeMixin
)
from perms import serializers from perms import serializers
from .mixin import AppRoleAdminMixin, AppRoleUserMixin from perms.tree.app import GrantedAppTreeUtil
from perms.utils.application.user_permission import ( from perms.utils.application.user_permission import (
get_user_granted_all_applications get_user_granted_all_applications
) )
from .mixin import AppRoleAdminMixin, AppRoleUserMixin
__all__ = [ __all__ = [
@ -23,7 +23,7 @@ __all__ = [
] ]
class AllGrantedApplicationsMixin(CommonApiMixin, ListAPIView): class AllGrantedApplicationsApi(CommonApiMixin, ListAPIView):
only_fields = serializers.AppGrantedSerializer.Meta.only_fields only_fields = serializers.AppGrantedSerializer.Meta.only_fields
serializer_class = serializers.AppGrantedSerializer serializer_class = serializers.AppGrantedSerializer
filterset_fields = { filterset_fields = {
@ -41,28 +41,34 @@ class AllGrantedApplicationsMixin(CommonApiMixin, ListAPIView):
return queryset.only(*self.only_fields) return queryset.only(*self.only_fields)
class UserAllGrantedApplicationsApi(AppRoleAdminMixin, AllGrantedApplicationsMixin): class UserAllGrantedApplicationsApi(AppRoleAdminMixin, AllGrantedApplicationsApi):
pass pass
class MyAllGrantedApplicationsApi(AppRoleUserMixin, AllGrantedApplicationsMixin): class MyAllGrantedApplicationsApi(AppRoleUserMixin, AllGrantedApplicationsApi):
pass pass
class ApplicationsAsTreeMixin(SerializeApplicationToTreeNodeMixin): class ApplicationsAsTreeMixin:
""" """
将应用序列化成树的结构返回 将应用序列化成树的结构返回
""" """
serializer_class = TreeNodeSerializer serializer_class = TreeNodeSerializer
user: None user: None
filter_queryset: Callable
get_queryset: Callable
get_serializer: Callable
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
tree_id = request.query_params.get('tree_id', None) tree_id = request.query_params.get('tree_id', None)
parent_info = request.query_params.get('parentInfo', None) parent_info = request.query_params.get('parentInfo', None)
queryset = self.filter_queryset(self.get_queryset()) queryset = self.filter_queryset(self.get_queryset())
tree_nodes = self.serialize_applications_with_org( util = GrantedAppTreeUtil()
queryset, tree_id, parent_info, self.user
) 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) serializer = self.get_serializer(tree_nodes, many=True)
return Response(data=serializer.data) return Response(data=serializer.data)

View File

@ -36,7 +36,3 @@ class AssetRoleUserMixin(PermBaseMixin, _RoleUserMixin):
('get_tree', 'perms.view_myassets'), ('get_tree', 'perms.view_myassets'),
('GET', 'perms.view_myassets'), ('GET', 'perms.view_myassets'),
) )
def dispatch(self, *args, **kwargs):
with tmp_to_root_org():
return super().dispatch(*args, **kwargs)

View File

@ -6,16 +6,12 @@ from rest_framework.generics import get_object_or_404
from common.tree import TreeNode from common.tree import TreeNode
from orgs.models import Organization from orgs.models import Organization
from assets.models import SystemUser 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 perms.utils.application.permission import get_application_system_user_ids
from ..models import Application
__all__ = ['SerializeApplicationToTreeNodeMixin']
class SerializeApplicationToTreeNodeMixin:
class GrantedAppTreeUtil:
@staticmethod @staticmethod
def filter_organizations(applications): def filter_organizations(applications):
organization_ids = set(applications.values_list('org_id', flat=True)) organization_ids = set(applications.values_list('org_id', flat=True))
@ -39,37 +35,15 @@ class SerializeApplicationToTreeNodeMixin:
}) })
return node 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 = [] 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)) parent_info = dict(parse_qsl(parent_info))
pod_name = parent_info.get('pod') pod_name = parent_info.get('pod')
app_id = parent_info.get('app_id') app_id = parent_info.get('app_id')
namespace = parent_info.get('namespace') namespace = parent_info.get('namespace')
system_user_id = parent_info.get('system_user_id') system_user_id = parent_info.get('system_user_id')
if app_id and not any([pod_name, namespace, system_user_id]): if app_id and not any([pod_name, namespace, system_user_id]):
app = get_object_or_404(Application, id=app_id) app = get_object_or_404(Application, id=app_id)
system_user_ids = get_application_system_user_ids(user, app) system_user_ids = get_application_system_user_ids(user, app)
@ -80,6 +54,34 @@ class SerializeApplicationToTreeNodeMixin:
) )
tree_nodes.append(system_user_node) tree_nodes.append(system_user_node)
return tree_nodes return tree_nodes
tree_nodes = KubernetesTree(tree_id).async_tree_node(parent_info) tree_nodes = KubernetesTree(tree_id).async_tree_node(parent_info)
return tree_nodes 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

View File

@ -202,7 +202,9 @@ class UserGrantedTreeRefreshController:
user = self.user user = self.user
with tmp_to_root_org(): 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(): if force or self.have_need_refresh_orgs():
with UserGrantedTreeRebuildLock(user_id=user.id): with UserGrantedTreeRebuildLock(user_id=user.id):
@ -219,7 +221,9 @@ class UserGrantedTreeRefreshController:
utils = UserGrantedTreeBuildUtils(user) utils = UserGrantedTreeBuildUtils(user)
utils.rebuild_user_granted_tree() utils.rebuild_user_granted_tree()
logger.info( 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: class UserGrantedUtilsBase:

View File

@ -16,7 +16,7 @@ class RBACBackend(JMSBaseAuthBackend):
return False return False
def has_perm(self, user_obj, perm, obj=None): 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() raise PermissionDenied()
if perm == '*': if perm == '*':
return True return True

View File

@ -5,7 +5,7 @@ from .const import Scope, system_exclude_permissions, org_exclude_permissions
# Todo: 获取应该区分 系统用户,和组织用户的权限 # Todo: 获取应该区分 系统用户,和组织用户的权限
# 工作台也区分组织后再考虑 # 工作台也区分组织后再考虑
user_perms = ( user_perms = (
('rbac', 'menupermission', 'view', 'workspace'), ('rbac', 'menupermission', 'view', 'workbench'),
('rbac', 'menupermission', 'view', 'webterminal'), ('rbac', 'menupermission', 'view', 'webterminal'),
('rbac', 'menupermission', 'view', 'filemanager'), ('rbac', 'menupermission', 'view', 'filemanager'),
('perms', 'permedasset', 'view,connect', 'myassets'), ('perms', 'permedasset', 'view,connect', 'myassets'),
@ -16,7 +16,9 @@ user_perms = (
('applications', 'application', 'match', 'application'), ('applications', 'application', 'match', 'application'),
('ops', 'commandexecution', 'add', 'commandexecution'), ('ops', 'commandexecution', 'add', 'commandexecution'),
('authentication', 'connectiontoken', 'add', 'connectiontoken'), ('authentication', 'connectiontoken', 'add', 'connectiontoken'),
('authentication', 'temptoken', 'add', 'temptoken'),
('tickets', 'ticket', 'view', 'ticket'), ('tickets', 'ticket', 'view', 'ticket'),
('orgs', 'organization', 'view', 'rootorg'),
) )
auditor_perms = user_perms + ( auditor_perms = user_perms + (
@ -29,7 +31,6 @@ auditor_perms = user_perms + (
('ops', 'commandexecution', 'view', 'commandexecution') ('ops', 'commandexecution', 'view', 'commandexecution')
) )
app_exclude_perms = [ app_exclude_perms = [
('users', 'user', 'add,delete', 'user'), ('users', 'user', 'add,delete', 'user'),
('orgs', 'org', 'add,delete,change', 'org'), ('orgs', 'org', 'add,delete,change', 'org'),
@ -59,7 +60,8 @@ class PredefineRole:
from rbac.models import Role from rbac.models import Role
return Role.objects.get(id=self.id) return Role.objects.get(id=self.id)
def _get_defaults(self): @property
def default_perms(self):
from rbac.models import Permission from rbac.models import Permission
q = Permission.get_define_permissions_q(self.perms) q = Permission.get_define_permissions_q(self.perms)
permissions = Permission.get_permissions(self.scope) permissions = Permission.get_permissions(self.scope)
@ -72,9 +74,13 @@ class PredefineRole:
permissions = permissions.exclude(q) permissions = permissions.exclude(q)
perms = permissions.values_list('id', flat=True) perms = permissions.values_list('id', flat=True)
return perms
def _get_defaults(self):
perms = self.default_perms
defaults = { defaults = {
'id': self.id, 'name': self.name, 'scope': self.scope, 'id': self.id, 'name': self.name, 'scope': self.scope,
'builtin': True, 'permissions': perms 'builtin': True, 'permissions': perms, 'created_by': 'System',
} }
return defaults return defaults

View File

@ -22,7 +22,6 @@ exclude_permissions = (
('common', 'setting', '*', '*'), ('common', 'setting', '*', '*'),
('authentication', 'privatetoken', '*', '*'), ('authentication', 'privatetoken', '*', '*'),
('authentication', 'accesskey', 'change,delete', 'accesskey'),
('authentication', 'connectiontoken', 'change,delete', 'connectiontoken'), ('authentication', 'connectiontoken', 'change,delete', 'connectiontoken'),
('authentication', 'ssotoken', '*', '*'), ('authentication', 'ssotoken', '*', '*'),
('authentication', 'superconnectiontoken', 'change,delete', 'superconnectiontoken'), ('authentication', 'superconnectiontoken', 'change,delete', 'superconnectiontoken'),
@ -49,6 +48,8 @@ exclude_permissions = (
('rbac', 'contenttype', '*', '*'), ('rbac', 'contenttype', '*', '*'),
('rbac', 'permission', 'add,delete,change', 'permission'), ('rbac', 'permission', 'add,delete,change', 'permission'),
('rbac', 'rolebinding', '*', '*'), ('rbac', 'rolebinding', '*', '*'),
('rbac', 'systemrolebinding', 'change', 'systemrolebinding'),
('rbac', 'orgrolebinding', 'change', 'orgrolebinding'),
('rbac', 'role', '*', '*'), ('rbac', 'role', '*', '*'),
('ops', 'adhoc', 'delete,change', '*'), ('ops', 'adhoc', 'delete,change', '*'),
('ops', 'adhocexecution', 'add,delete,change', '*'), ('ops', 'adhocexecution', 'add,delete,change', '*'),
@ -99,6 +100,7 @@ only_system_permissions = (
('orgs', 'organization', '*', '*'), ('orgs', 'organization', '*', '*'),
('xpack', 'license', '*', '*'), ('xpack', 'license', '*', '*'),
('settings', 'setting', '*', '*'), ('settings', 'setting', '*', '*'),
('tickets', '*', '*', '*'),
('ops', 'task', 'view', 'taskmonitor'), ('ops', 'task', 'view', 'taskmonitor'),
('terminal', 'terminal', '*', '*'), ('terminal', 'terminal', '*', '*'),
('terminal', 'commandstorage', '*', '*'), ('terminal', 'commandstorage', '*', '*'),
@ -106,6 +108,7 @@ only_system_permissions = (
('terminal', 'status', '*', '*'), ('terminal', 'status', '*', '*'),
('terminal', 'task', '*', '*'), ('terminal', 'task', '*', '*'),
('authentication', '*', '*', '*'), ('authentication', '*', '*', '*'),
('tickets', '*', '*', '*'),
) )
only_org_permissions = ( only_org_permissions = (

View File

@ -27,7 +27,7 @@ class Migration(migrations.Migration):
], ],
options={ options={
'verbose_name': 'Menu permission', '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': [], 'default_permissions': [],
}, },
), ),

View File

@ -12,6 +12,6 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='menupermission', 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'},
), ),
] ]

View File

@ -12,6 +12,6 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='menupermission', 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'},
), ),
] ]

View File

@ -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'},
),
]

View File

@ -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)
]

View File

@ -14,7 +14,7 @@ class MenuPermission(models.Model):
permissions = [ permissions = [
('view_console', _('Can view console view')), ('view_console', _('Can view console view')),
('view_audit', _('Can view audit 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_webterminal', _('Can view web terminal')),
('view_filemanager', _('Can view file manager')), ('view_filemanager', _('Can view file manager')),
] ]

View File

@ -118,6 +118,9 @@ class Role(JMSModel):
return self.name return self.name
return gettext(self.name) return gettext(self.name)
def is_org(self):
return self.scope == const.Scope.org
class SystemRole(Role): class SystemRole(Role):
objects = SystemRoleManager() objects = SystemRoleManager()

View File

@ -1,6 +1,7 @@
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.db import models from django.db import models
from django.db.models import Q from django.db.models import Q
from django.core.exceptions import ValidationError
from rest_framework.serializers import ValidationError from rest_framework.serializers import ValidationError
from common.db.models import JMSModel from common.db.models import JMSModel
@ -67,6 +68,7 @@ class RoleBinding(JMSModel):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.scope = self.role.scope self.scope = self.role.scope
self.clean()
return super().save(*args, **kwargs) return super().save(*args, **kwargs)
@classmethod @classmethod
@ -95,6 +97,9 @@ class RoleBinding(JMSModel):
def role_display(self): def role_display(self):
return self.role.display_name return self.role.display_name
def is_scope_org(self):
return self.scope == Scope.org
class OrgRoleBindingManager(RoleBindingManager): class OrgRoleBindingManager(RoleBindingManager):
def get_queryset(self): def get_queryset(self):
@ -147,3 +152,12 @@ class SystemRoleBinding(RoleBinding):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.scope = Scope.system self.scope = Scope.system
return super().save(*args, **kwargs) 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)

View File

@ -93,7 +93,7 @@ class RBACPermission(permissions.DjangoModelPermissions):
try: try:
queryset = self._queryset(view) queryset = self._queryset(view)
model_cls = queryset.model model_cls = queryset.model
except AssertionError: except:
model_cls = None model_cls = None
return model_cls return model_cls

View File

@ -1,7 +1,7 @@
#!/usr/bin/python #!/usr/bin/python
import os
from collections import defaultdict from collections import defaultdict
from typing import Callable from typing import Callable
import os
from django.utils.translation import gettext_lazy as _, gettext, get_language from django.utils.translation import gettext_lazy as _, gettext, get_language
from django.conf import settings from django.conf import settings
@ -24,7 +24,7 @@ root_node_data = {
# 第二层 view 节点,手动创建的 # 第二层 view 节点,手动创建的
view_nodes_data = [ view_nodes_data = [
{'id': 'view_console', 'name': _('Console view')}, {'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_audit', 'name': _('Audit view')},
{'id': 'view_setting', 'name': _('System setting')}, {'id': 'view_setting', 'name': _('System setting')},
{'id': 'view_other', 'name': _('Other')}, {'id': 'view_other', 'name': _('Other')},
@ -55,8 +55,8 @@ extra_nodes_data = [
{"id": "app_change_plan_node", "name": _("App change auth"), "pId": "accounts"}, {"id": "app_change_plan_node", "name": _("App change auth"), "pId": "accounts"},
{"id": "asset_change_plan_node", "name": _("Asset 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": "terminal_node", "name": _("Terminal setting"), "pId": "view_setting"},
{'id': "my_assets", "name": _("My assets"), "pId": "view_workspace"}, {'id': "my_assets", "name": _("My assets"), "pId": "view_workbench"},
{'id': "my_apps", "name": _("My apps"), "pId": "view_workspace"}, {'id': "my_apps", "name": _("My apps"), "pId": "view_workbench"},
] ]
# 将 model 放到其它节点下,而不是本来的 app 中 # 将 model 放到其它节点下,而不是本来的 app 中
@ -87,10 +87,9 @@ special_pid_mapper = {
'terminal.status': 'terminal_node', 'terminal.status': 'terminal_node',
'terminal.task': 'terminal_node', 'terminal.task': 'terminal_node',
'audits.ftplog': 'terminal', 'audits.ftplog': 'terminal',
'rbac.menupermission': 'view_other',
'perms.view_myassets': 'my_assets', 'perms.view_myassets': 'my_assets',
'perms.view_myapps': 'my_apps', 'perms.view_myapps': 'my_apps',
'ops.add_commandexecution': 'view_workspace', 'ops.add_commandexecution': 'view_workbench',
'ops.view_commandexecution': 'audits', 'ops.view_commandexecution': 'audits',
"perms.view_mykubernetsapp": "my_apps", "perms.view_mykubernetsapp": "my_apps",
"perms.connect_mykubernetsapp": "my_apps", "perms.connect_mykubernetsapp": "my_apps",
@ -103,9 +102,9 @@ special_pid_mapper = {
"settings.view_setting": "view_setting", "settings.view_setting": "view_setting",
"rbac.view_console": "view_console", "rbac.view_console": "view_console",
"rbac.view_audit": "view_audit", "rbac.view_audit": "view_audit",
"rbac.view_workspace": "view_workspace", "rbac.view_workbench": "view_workbench",
"rbac.view_webterminal": "view_workspace", "rbac.view_webterminal": "view_workbench",
"rbac.view_filemanager": "view_workspace", "rbac.view_filemanager": "view_workbench",
'tickets.view_ticket': 'tickets' 'tickets.view_ticket': 'tickets'
} }

View File

@ -195,7 +195,9 @@ class LDAPUserImportAPI(APIView):
def get_ldap_users(self): def get_ldap_users(self):
username_list = self.request.data.get('username_list', []) username_list = self.request.data.get('username_list', [])
cache_police = self.request.query_params.get('cache_police', True) 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) users = LDAPCacheUtil().search(search_users=username_list)
else: else:
users = LDAPServerUtil().search(search_users=username_list) users = LDAPServerUtil().search(search_users=username_list)
@ -234,4 +236,3 @@ class LDAPCacheRefreshAPI(generics.RetrieveAPIView):
logger.error(str(e)) logger.error(str(e))
return Response(data={'msg': str(e)}, status=400) return Response(data={'msg': str(e)}, status=400)
return Response(data={'msg': 'success'}) return Response(data={'msg': 'success'})

View File

@ -30,19 +30,15 @@ class PublicSettingApi(generics.RetrieveAPIView):
def get_object(self): def get_object(self):
instance = { instance = {
"data": { "data": {
# Security
"WINDOWS_SKIP_ALL_MANUAL_PASSWORD": settings.WINDOWS_SKIP_ALL_MANUAL_PASSWORD, "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, "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_VIEW_AUTH_NEED_MFA": settings.SECURITY_VIEW_AUTH_NEED_MFA,
"SECURITY_MFA_VERIFY_TTL": settings.SECURITY_MFA_VERIFY_TTL, "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_COMMAND_EXECUTION": settings.SECURITY_COMMAND_EXECUTION,
"SECURITY_PASSWORD_EXPIRATION_TIME": settings.SECURITY_PASSWORD_EXPIRATION_TIME, "SECURITY_PASSWORD_EXPIRATION_TIME": settings.SECURITY_PASSWORD_EXPIRATION_TIME,
"SECURITY_LUNA_REMEMBER_AUTH": settings.SECURITY_LUNA_REMEMBER_AUTH, "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": { "PASSWORD_RULE": {
'SECURITY_PASSWORD_MIN_LENGTH': settings.SECURITY_PASSWORD_MIN_LENGTH, 'SECURITY_PASSWORD_MIN_LENGTH': settings.SECURITY_PASSWORD_MIN_LENGTH,
'SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH': settings.SECURITY_ADMIN_USER_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_NUMBER': settings.SECURITY_PASSWORD_NUMBER,
'SECURITY_PASSWORD_SPECIAL_CHAR': settings.SECURITY_PASSWORD_SPECIAL_CHAR, '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_WECOM": settings.AUTH_WECOM,
"AUTH_DINGTALK": settings.AUTH_DINGTALK, "AUTH_DINGTALK": settings.AUTH_DINGTALK,
"AUTH_FEISHU": settings.AUTH_FEISHU, "AUTH_FEISHU": settings.AUTH_FEISHU,
'SECURITY_WATERMARK_ENABLED': settings.SECURITY_WATERMARK_ENABLED, # Terminal
'SECURITY_SESSION_SHARE': settings.SECURITY_SESSION_SHARE,
"XRDP_ENABLED": settings.XRDP_ENABLED, "XRDP_ENABLED": settings.XRDP_ENABLED,
# Announcement
"ANNOUNCEMENT_ENABLED": settings.ANNOUNCEMENT_ENABLED, "ANNOUNCEMENT_ENABLED": settings.ANNOUNCEMENT_ENABLED,
"ANNOUNCEMENT": settings.ANNOUNCEMENT, "ANNOUNCEMENT": settings.ANNOUNCEMENT,
"HELP_DOCUMENT_URL": settings.HELP_DOCUMENT_URL, "AUTH_TEMP_TOKEN": settings.AUTH_TEMP_TOKEN,
"HELP_SUPPORT_URL": settings.HELP_SUPPORT_URL,
} }
} }
return instance return instance

View File

@ -1,4 +1,3 @@
import json
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
@ -10,10 +9,19 @@ __all__ = [
class CASSettingSerializer(serializers.Serializer): class CASSettingSerializer(serializers.Serializer):
AUTH_CAS = serializers.BooleanField(required=False, label=_('Enable CAS Auth')) AUTH_CAS = serializers.BooleanField(required=False, label=_('Enable CAS Auth'))
CAS_SERVER_URL = serializers.CharField(required=False, max_length=1024, label=_('Server url')) 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_LOGOUT_COMPLETELY = serializers.BooleanField(required=False, label=_('Logout completely'))
CAS_VERSION = serializers.IntegerField(required=False, label=_('Version'), min_value=1, max_value=3) CAS_VERSION = serializers.IntegerField(
CAS_USERNAME_ATTRIBUTE = serializers.CharField(required=False, max_length=1024, label=_('Username attr')) required=False, label=_('Version'), min_value=1, max_value=3
CAS_APPLY_ATTRIBUTES_TO_USER = serializers.BooleanField(required=False, label=_('Enable attributes map')) )
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_RENAME_ATTRIBUTES = serializers.DictField(required=False, label=_('Rename attr'))
CAS_CREATE_USER = serializers.BooleanField(required=False, label=_('Create user if not')) CAS_CREATE_USER = serializers.BooleanField(required=False, label=_('Create user if not'))

View File

@ -1,4 +1,3 @@
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
@ -40,8 +39,9 @@ class LDAPSettingSerializer(serializers.Serializer):
help_text=_('eg: ldap://localhost:389') help_text=_('eg: ldap://localhost:389')
) )
AUTH_LDAP_BIND_DN = serializers.CharField(required=False, max_length=1024, label=_('Bind DN')) 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, AUTH_LDAP_BIND_PASSWORD = serializers.CharField(
label=_('Password')) max_length=1024, write_only=True, required=False, label=_('Password')
)
AUTH_LDAP_SEARCH_OU = serializers.CharField( AUTH_LDAP_SEARCH_OU = serializers.CharField(
max_length=1024, allow_blank=True, required=False, label=_('User OU'), max_length=1024, allow_blank=True, required=False, label=_('User OU'),
help_text=_('Use | split multi OUs') 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 ' help_text=_('User attr map present how to map LDAP user attr to '
'jumpserver, username,name,email is jumpserver attr') '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( AUTH_LDAP_SYNC_IS_PERIODIC = serializers.BooleanField(
required=False, label=_('Periodic perform') required=False, label=_('Periodic perform')
) )

View File

@ -22,17 +22,16 @@ class TerminalSettingSerializer(serializers.Serializer):
help_text=_('Tips: If use other auth method, like AD/LDAP, you should disable this to ' help_text=_('Tips: If use other auth method, like AD/LDAP, you should disable this to '
'avoid being able to log in after deleting') '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_SORT_BY = serializers.ChoiceField(
TERMINAL_ASSET_LIST_PAGE_SIZE = serializers.ChoiceField(PAGE_SIZE_CHOICES, required=False, SORT_BY_CHOICES, required=False, label=_('List sort by')
label=_('List page size')) )
TERMINAL_ASSET_LIST_PAGE_SIZE = serializers.ChoiceField(
PAGE_SIZE_CHOICES, required=False, label=_('List page size')
)
TERMINAL_TELNET_REGEX = serializers.CharField( TERMINAL_TELNET_REGEX = serializers.CharField(
allow_blank=True, max_length=1024, required=False, label=_('Telnet login regex'), allow_blank=True, max_length=1024, required=False, label=_('Telnet login regex'),
help_text=_("The login success message varies with devices. " help_text=_("The login success message varies with devices. "
"if you cannot log in to the device through Telnet, set this parameter") "if you cannot log in to the device through Telnet, set this parameter")
) )
TERMINAL_RDP_ADDR = serializers.CharField( TERMINAL_MAGNUS_ENABLED = serializers.BooleanField(label=_("Enable database proxy"))
required=False, label=_("RDP address"), max_length=1024, allow_blank=True,
help_text=_('RDP visit address, eg: dev.jumpserver.org:3389')
)
XRDP_ENABLED = serializers.BooleanField(label=_("Enable XRDP")) XRDP_ENABLED = serializers.BooleanField(label=_("Enable XRDP"))

View File

@ -3,6 +3,8 @@
import json import json
import threading import threading
from django.conf import LazySettings
from django.db.utils import ProgrammingError, OperationalError
from django.dispatch import receiver from django.dispatch import receiver
from django.db.models.signals import post_save, pre_save from django.db.models.signals import post_save, pre_save
from django.utils.functional import LazyObject 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 = threading.Thread(target=keep_subscribe_settings_change)
t.daemon = True t.daemon = True
t.start() 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

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -52,6 +52,12 @@
<span> English</span> <span> English</span>
</a> </a>
</li> </li>
<li>
<a id="switch_ja" href="{% url 'i18n-switch' lang='ja' %}">
<i class="fa fa-flag-checkered"></i>
<span> 日本語</span>
</a>
</li>
</ul> </ul>
</li> </li>

View File

@ -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>

View File

@ -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>

View File

@ -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 %}

View File

@ -7,3 +7,4 @@ from .task import *
from .storage import * from .storage import *
from .status import * from .status import *
from .sharing import * from .sharing import *
from .endpoint import *

View File

@ -1,20 +1,17 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import time
from django.conf import settings from django.conf import settings
from django.utils import timezone from django.utils import timezone
from django.shortcuts import HttpResponse
from rest_framework import generics from rest_framework import generics
from rest_framework.fields import DateTimeField from rest_framework.fields import DateTimeField
from rest_framework.response import Response from rest_framework.response import Response
from django.template import loader
from terminal.models import CommandStorage, Session, Command from terminal.models import CommandStorage, Session, Command
from terminal.filters import CommandFilter from terminal.filters import CommandFilter
from orgs.utils import current_org from orgs.utils import current_org
from common.drf.api import JMSBulkModelViewSet from common.drf.api import JMSBulkModelViewSet
from common.utils import get_logger from common.utils import get_logger
from terminal.serializers import InsecureCommandAlertSerializer from terminal.backends.command.serializers import InsecureCommandAlertSerializer
from terminal.exceptions import StorageInvalid from terminal.exceptions import StorageInvalid
from ..backends import ( from ..backends import (
get_command_storage, get_multi_command_storage, get_command_storage, get_multi_command_storage,
@ -23,7 +20,7 @@ from ..backends import (
from ..notifications import CommandAlertMessage from ..notifications import CommandAlertMessage
logger = get_logger(__name__) logger = get_logger(__name__)
__all__ = ['CommandViewSet', 'CommandExportApi', 'InsecureCommandAlertAPI'] __all__ = ['CommandViewSet', 'InsecureCommandAlertAPI']
class CommandQueryMixin: class CommandQueryMixin:
@ -191,29 +188,6 @@ class CommandViewSet(JMSBulkModelViewSet):
return Response({"msg": msg}, status=401) 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): class InsecureCommandAlertAPI(generics.CreateAPIView):
serializer_class = InsecureCommandAlertSerializer serializer_class = InsecureCommandAlertSerializer
rbac_perms = { rbac_perms = {

View File

@ -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()

View File

@ -42,7 +42,6 @@ class MySessionAPIView(generics.ListAPIView):
serializer_class = serializers.SessionSerializer serializer_class = serializers.SessionSerializer
def get_queryset(self): def get_queryset(self):
with tmp_to_root_org():
user = self.request.user user = self.request.user
qs = Session.objects.filter(user_id=user.id) qs = Session.objects.filter(user_id=user.id)
return qs return qs

View File

@ -12,7 +12,7 @@ from django.utils.translation import gettext_lazy as _
from common.exceptions import JMSException from common.exceptions import JMSException
from common.drf.api import JMSBulkModelViewSet 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 common.permissions import WithBootstrapToken
from ..models import Terminal from ..models import Terminal
from .. import serializers from .. import serializers

View File

@ -4,6 +4,7 @@ import datetime
from django.db import transaction from django.db import transaction
from django.utils import timezone from django.utils import timezone
from django.db.utils import OperationalError from django.db.utils import OperationalError
from common.utils.common import pretty_string
from .base import CommandBase from .base import CommandBase
@ -32,9 +33,11 @@ class CommandStore(CommandBase):
""" """
_commands = [] _commands = []
for c in commands: for c in commands:
cmd_input = pretty_string(c['input'])
cmd_output = pretty_string(c['output'], max_length=1024)
_commands.append(self.model( _commands.append(self.model(
user=c["user"], asset=c["asset"], system_user=c["system_user"], 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"], risk_level=c.get("risk_level", 0), org_id=c["org_id"],
timestamp=c["timestamp"] timestamp=c["timestamp"]
)) ))

View File

@ -18,7 +18,6 @@ from common.utils import get_logger
from common.exceptions import JMSException from common.exceptions import JMSException
from .models import AbstractSessionCommand from .models import AbstractSessionCommand
logger = get_logger(__file__) logger = get_logger(__file__)
@ -27,7 +26,7 @@ class InvalidElasticsearch(JMSException):
default_detail = _('Invalid elasticsearch config') default_detail = _('Invalid elasticsearch config')
class CommandStore(): class CommandStore(object):
def __init__(self, config): def __init__(self, config):
hosts = config.get("HOSTS") hosts = config.get("HOSTS")
kwargs = config.get("OTHER", {}) kwargs = config.get("OTHER", {})

View File

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from datetime import datetime
import uuid import uuid
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -28,6 +29,10 @@ class AbstractSessionCommand(OrgModelMixin):
class Meta: class Meta:
abstract = True abstract = True
@lazyproperty
def timestamp_display(self):
return datetime.fromtimestamp(self.timestamp)
@lazyproperty @lazyproperty
def remote_addr(self): def remote_addr(self):
from terminal.models import Session from terminal.models import Session

View File

@ -4,27 +4,19 @@ from rest_framework import serializers
from .models import AbstractSessionCommand 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 user = serializers.CharField(label=_("User")) # 限制 64 字符,见 validate_user
asset = serializers.CharField(max_length=128, label=_("Asset")) asset = serializers.CharField(max_length=128, label=_("Asset"))
system_user = serializers.CharField(max_length=64, label=_("System user")) input = serializers.CharField(max_length=2048, label=_("Command"))
input = serializers.CharField(max_length=128, label=_("Command"))
output = serializers.CharField(max_length=1024, allow_blank=True, label=_("Output"))
session = serializers.CharField(max_length=36, label=_("Session ID")) 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 = serializers.ChoiceField(
risk_level_display = serializers.SerializerMethodField(label=_('Risk level display')) 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) 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): def validate_user(self, value):
if len(value) > 64: if len(value) > 64:
@ -32,9 +24,22 @@ class SessionCommandSerializer(serializers.Serializer):
return value return value
class InsecureCommandAlertSerializer(serializers.Serializer): class InsecureCommandAlertSerializer(SimpleSessionCommandSerializer):
input = serializers.CharField() pass
asset = serializers.CharField()
user = serializers.CharField()
risk_level = serializers.IntegerField() class SessionCommandSerializer(SimpleSessionCommandSerializer):
session = serializers.UUIDField() """使用这个类作为基础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