mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-12-16 17:12:53 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df05973da3 | ||
|
|
d2a4e79b31 | ||
|
|
f65e2ac15f | ||
|
|
96f69c821b | ||
|
|
13fffa52dd | ||
|
|
fe37913ed9 | ||
|
|
4009457000 |
@@ -25,7 +25,7 @@ def migrate_asset_accounts(apps, schema_editor):
|
|||||||
count += len(auth_books)
|
count += len(auth_books)
|
||||||
# auth book 和 account 相同的属性
|
# auth book 和 account 相同的属性
|
||||||
same_attrs = [
|
same_attrs = [
|
||||||
'id', 'username', 'comment', 'date_created', 'date_updated',
|
'username', 'comment', 'date_created', 'date_updated',
|
||||||
'created_by', 'asset_id', 'org_id',
|
'created_by', 'asset_id', 'org_id',
|
||||||
]
|
]
|
||||||
# 认证的属性,可能是 auth_book 的,可能是 system_user 的
|
# 认证的属性,可能是 auth_book 的,可能是 system_user 的
|
||||||
|
|||||||
@@ -403,12 +403,7 @@ class NodeAssetsMixin(NodeAllAssetsMappingMixin):
|
|||||||
return Asset.objects.filter(q).distinct()
|
return Asset.objects.filter(q).distinct()
|
||||||
|
|
||||||
def get_assets_amount(self):
|
def get_assets_amount(self):
|
||||||
q = Q(node__key__startswith=f'{self.key}:') | Q(node__key=self.key)
|
return self.get_all_assets().count()
|
||||||
return self.assets.through.objects.filter(q).count()
|
|
||||||
|
|
||||||
def get_assets_account_by_children(self):
|
|
||||||
children = self.get_all_children().values_list()
|
|
||||||
return self.assets.through.objects.filter(node_id__in=children).count()
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_node_all_assets_by_key_v2(cls, key):
|
def get_node_all_assets_by_key_v2(cls, key):
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
from common.permissions import ServiceAccountSignaturePermission
|
||||||
from .base import JMSBaseAuthBackend
|
from .base import JMSBaseAuthBackend
|
||||||
|
|
||||||
UserModel = get_user_model()
|
UserModel = get_user_model()
|
||||||
@@ -18,6 +19,10 @@ class PublicKeyAuthBackend(JMSBaseAuthBackend):
|
|||||||
def authenticate(self, request, username=None, public_key=None, **kwargs):
|
def authenticate(self, request, username=None, public_key=None, **kwargs):
|
||||||
if not public_key:
|
if not public_key:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
permission = ServiceAccountSignaturePermission()
|
||||||
|
if not permission.has_permission(request, None):
|
||||||
|
return None
|
||||||
if username is None:
|
if username is None:
|
||||||
username = kwargs.get(UserModel.USERNAME_FIELD)
|
username = kwargs.get(UserModel.USERNAME_FIELD)
|
||||||
try:
|
try:
|
||||||
@@ -26,7 +31,7 @@ class PublicKeyAuthBackend(JMSBaseAuthBackend):
|
|||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
if user.check_public_key(public_key) and \
|
if user.check_public_key(public_key) and \
|
||||||
self.user_can_authenticate(user):
|
self.user_can_authenticate(user):
|
||||||
return user
|
return user
|
||||||
|
|
||||||
def get_user(self, user_id):
|
def get_user(self, user_id):
|
||||||
|
|||||||
@@ -86,3 +86,38 @@ class UserConfirmation(permissions.BasePermission):
|
|||||||
min_level = ConfirmType.values.index(confirm_type) + 1
|
min_level = ConfirmType.values.index(confirm_type) + 1
|
||||||
name = 'UserConfirmationLevel{}TTL{}'.format(min_level, ttl)
|
name = 'UserConfirmationLevel{}TTL{}'.format(min_level, ttl)
|
||||||
return type(name, (cls,), {'min_level': min_level, 'ttl': ttl, 'confirm_type': confirm_type})
|
return type(name, (cls,), {'min_level': min_level, 'ttl': ttl, 'confirm_type': confirm_type})
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceAccountSignaturePermission(permissions.BasePermission):
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
from authentication.models import AccessKey
|
||||||
|
from common.utils.crypto import get_aes_crypto
|
||||||
|
signature = request.META.get('HTTP_X_JMS_SVC', '')
|
||||||
|
if not signature or not signature.startswith('Sign'):
|
||||||
|
return False
|
||||||
|
data = signature[4:].strip()
|
||||||
|
if not data or ':' not in data:
|
||||||
|
return False
|
||||||
|
ak_id, time_sign = data.split(':', 1)
|
||||||
|
if not ak_id or not time_sign:
|
||||||
|
return False
|
||||||
|
ak = AccessKey.objects.filter(id=ak_id).first()
|
||||||
|
if not ak or not ak.is_active:
|
||||||
|
return False
|
||||||
|
if not ak.user or not ak.user.is_active or not ak.user.is_service_account:
|
||||||
|
return False
|
||||||
|
aes = get_aes_crypto(str(ak.secret).replace('-', ''), mode='ECB')
|
||||||
|
try:
|
||||||
|
timestamp = aes.decrypt(time_sign)
|
||||||
|
if not timestamp or not timestamp.isdigit():
|
||||||
|
return False
|
||||||
|
timestamp = int(timestamp)
|
||||||
|
interval = abs(int(time.time()) - timestamp)
|
||||||
|
if interval > 30:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def has_object_permission(self, request, view, obj):
|
||||||
|
return False
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
import struct
|
|
||||||
import random
|
import random
|
||||||
import socket
|
import socket
|
||||||
import string
|
import string
|
||||||
|
import struct
|
||||||
|
|
||||||
string_punctuation = '!#$%&()*+,-.:;<=>?@[]^_~'
|
string_punctuation = '!#$%&()*+,-.:;<=>?@[]^_~'
|
||||||
|
|
||||||
@@ -19,6 +18,7 @@ def random_ip():
|
|||||||
|
|
||||||
|
|
||||||
def random_string(length: int, lower=True, upper=True, digit=True, special_char=False):
|
def random_string(length: int, lower=True, upper=True, digit=True, special_char=False):
|
||||||
|
random.seed(None)
|
||||||
args_names = ['lower', 'upper', 'digit', 'special_char']
|
args_names = ['lower', 'upper', 'digit', 'special_char']
|
||||||
args_values = [lower, upper, digit, special_char]
|
args_values = [lower, upper, digit, special_char]
|
||||||
args_string = [string.ascii_lowercase, string.ascii_uppercase, string.digits, string_punctuation]
|
args_string = [string.ascii_lowercase, string.ascii_uppercase, string.digits, string_punctuation]
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class SendAndVerifyCodeUtil(object):
|
|||||||
self.target = target
|
self.target = target
|
||||||
self.backend = backend
|
self.backend = backend
|
||||||
self.key = key or self.KEY_TMPL.format(target)
|
self.key = key or self.KEY_TMPL.format(target)
|
||||||
|
self.verify_key = self.key + '_verify'
|
||||||
self.timeout = settings.VERIFY_CODE_TTL if timeout is None else timeout
|
self.timeout = settings.VERIFY_CODE_TTL if timeout is None else timeout
|
||||||
self.other_args = kwargs
|
self.other_args = kwargs
|
||||||
|
|
||||||
@@ -47,6 +48,11 @@ class SendAndVerifyCodeUtil(object):
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
def verify(self, code):
|
def verify(self, code):
|
||||||
|
times = cache.get(self.verify_key, 0)
|
||||||
|
if times >= 3:
|
||||||
|
self.__clear()
|
||||||
|
raise CodeExpired
|
||||||
|
cache.set(self.verify_key, times + 1, timeout=self.timeout)
|
||||||
right = cache.get(self.key)
|
right = cache.get(self.key)
|
||||||
if not right:
|
if not right:
|
||||||
raise CodeExpired
|
raise CodeExpired
|
||||||
@@ -59,6 +65,7 @@ class SendAndVerifyCodeUtil(object):
|
|||||||
|
|
||||||
def __clear(self):
|
def __clear(self):
|
||||||
cache.delete(self.key)
|
cache.delete(self.key)
|
||||||
|
cache.delete(self.verify_key)
|
||||||
|
|
||||||
def __ttl(self):
|
def __ttl(self):
|
||||||
return cache.ttl(self.key)
|
return cache.ttl(self.key)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import shutil
|
|||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import SuspiciousFileOperation
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
@@ -18,6 +19,7 @@ __all__ = ["PlaybookViewSet", "PlaybookFileBrowserAPIView"]
|
|||||||
|
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
from django.utils._os import safe_join
|
||||||
|
|
||||||
|
|
||||||
def unzip_playbook(src, dist):
|
def unzip_playbook(src, dist):
|
||||||
@@ -37,7 +39,7 @@ class PlaybookViewSet(OrgBulkModelViewSet):
|
|||||||
raise JMSException(code='playbook_has_job', detail={"msg": _("Currently playbook is being used in a job")})
|
raise JMSException(code='playbook_has_job', detail={"msg": _("Currently playbook is being used in a job")})
|
||||||
instance_id = instance.id
|
instance_id = instance.id
|
||||||
super().perform_destroy(instance)
|
super().perform_destroy(instance)
|
||||||
dest_path = os.path.join(settings.DATA_DIR, "ops", "playbook", instance_id.__str__())
|
dest_path = safe_join(settings.DATA_DIR, "ops", "playbook", instance_id.__str__())
|
||||||
shutil.rmtree(dest_path)
|
shutil.rmtree(dest_path)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@@ -48,8 +50,8 @@ class PlaybookViewSet(OrgBulkModelViewSet):
|
|||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
instance = serializer.save()
|
instance = serializer.save()
|
||||||
if 'multipart/form-data' in self.request.headers['Content-Type']:
|
if 'multipart/form-data' in self.request.headers['Content-Type']:
|
||||||
src_path = os.path.join(settings.MEDIA_ROOT, instance.path.name)
|
src_path = safe_join(settings.MEDIA_ROOT, instance.path.name)
|
||||||
dest_path = os.path.join(settings.DATA_DIR, "ops", "playbook", instance.id.__str__())
|
dest_path = safe_join(settings.DATA_DIR, "ops", "playbook", instance.id.__str__())
|
||||||
try:
|
try:
|
||||||
unzip_playbook(src_path, dest_path)
|
unzip_playbook(src_path, dest_path)
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
@@ -60,9 +62,9 @@ class PlaybookViewSet(OrgBulkModelViewSet):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
if instance.create_method == 'blank':
|
if instance.create_method == 'blank':
|
||||||
dest_path = os.path.join(settings.DATA_DIR, "ops", "playbook", instance.id.__str__())
|
dest_path = safe_join(settings.DATA_DIR, "ops", "playbook", instance.id.__str__())
|
||||||
os.makedirs(dest_path)
|
os.makedirs(dest_path)
|
||||||
with open(os.path.join(dest_path, 'main.yml'), 'w') as f:
|
with open(safe_join(dest_path, 'main.yml'), 'w') as f:
|
||||||
f.write('## write your playbook here')
|
f.write('## write your playbook here')
|
||||||
|
|
||||||
|
|
||||||
@@ -83,13 +85,15 @@ class PlaybookFileBrowserAPIView(APIView):
|
|||||||
work_path = playbook.work_dir
|
work_path = playbook.work_dir
|
||||||
file_key = request.query_params.get('key', '')
|
file_key = request.query_params.get('key', '')
|
||||||
if file_key:
|
if file_key:
|
||||||
file_path = os.path.join(work_path, file_key)
|
try:
|
||||||
with open(file_path, 'r') as f:
|
file_path = safe_join(work_path, file_key)
|
||||||
try:
|
with open(file_path, 'r') as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
content = _('Unsupported file content')
|
content = _('Unsupported file content')
|
||||||
return Response({'content': content})
|
except SuspiciousFileOperation:
|
||||||
|
raise JMSException(code='invalid_file_path', detail={"msg": _("Invalid file path")})
|
||||||
|
return Response({'content': content})
|
||||||
else:
|
else:
|
||||||
expand_key = request.query_params.get('expand', '')
|
expand_key = request.query_params.get('expand', '')
|
||||||
nodes = self.generate_tree(playbook, work_path, expand_key)
|
nodes = self.generate_tree(playbook, work_path, expand_key)
|
||||||
@@ -105,7 +109,8 @@ class PlaybookFileBrowserAPIView(APIView):
|
|||||||
parent_key = ''
|
parent_key = ''
|
||||||
if os.path.dirname(parent_key) == 'root':
|
if os.path.dirname(parent_key) == 'root':
|
||||||
parent_key = os.path.basename(parent_key)
|
parent_key = os.path.basename(parent_key)
|
||||||
full_path = os.path.join(work_path, parent_key)
|
|
||||||
|
full_path = safe_join(work_path, parent_key)
|
||||||
|
|
||||||
is_directory = request.data.get('is_directory', False)
|
is_directory = request.data.get('is_directory', False)
|
||||||
content = request.data.get('content', '')
|
content = request.data.get('content', '')
|
||||||
@@ -117,27 +122,30 @@ class PlaybookFileBrowserAPIView(APIView):
|
|||||||
p = 'new_file.yml'
|
p = 'new_file.yml'
|
||||||
else:
|
else:
|
||||||
p = 'new_dir'
|
p = 'new_dir'
|
||||||
np = os.path.join(full_path, p)
|
np = safe_join(full_path, p)
|
||||||
n = 0
|
n = 0
|
||||||
while os.path.exists(np):
|
while os.path.exists(np):
|
||||||
n += 1
|
n += 1
|
||||||
np = os.path.join(full_path, '{}({})'.format(p, n))
|
np = safe_join(full_path, '{}({})'.format(p, n))
|
||||||
return np
|
return np
|
||||||
|
|
||||||
if is_directory:
|
try:
|
||||||
new_file_path = find_new_name(name)
|
if is_directory:
|
||||||
os.makedirs(new_file_path)
|
new_file_path = find_new_name(name)
|
||||||
else:
|
os.makedirs(new_file_path)
|
||||||
new_file_path = find_new_name(name, True)
|
else:
|
||||||
with open(new_file_path, 'w') as f:
|
new_file_path = find_new_name(name, True)
|
||||||
f.write(content)
|
with open(new_file_path, 'w') as f:
|
||||||
|
f.write(content)
|
||||||
|
except SuspiciousFileOperation:
|
||||||
|
raise JMSException(code='invalid_file_path', detail={"msg": _("Invalid file path")})
|
||||||
|
|
||||||
relative_path = os.path.relpath(os.path.dirname(new_file_path), work_path)
|
relative_path = os.path.relpath(os.path.dirname(new_file_path), work_path)
|
||||||
new_node = {
|
new_node = {
|
||||||
"name": os.path.basename(new_file_path),
|
"name": os.path.basename(new_file_path),
|
||||||
"title": os.path.basename(new_file_path),
|
"title": os.path.basename(new_file_path),
|
||||||
"id": os.path.join(relative_path, os.path.basename(new_file_path))
|
"id": safe_join(relative_path, os.path.basename(new_file_path))
|
||||||
if not os.path.join(relative_path, os.path.basename(new_file_path)).startswith('.')
|
if not safe_join(relative_path, os.path.basename(new_file_path)).startswith('.')
|
||||||
else os.path.basename(new_file_path),
|
else os.path.basename(new_file_path),
|
||||||
"isParent": is_directory,
|
"isParent": is_directory,
|
||||||
"pId": relative_path if not relative_path.startswith('.') else 'root',
|
"pId": relative_path if not relative_path.startswith('.') else 'root',
|
||||||
@@ -156,7 +164,7 @@ class PlaybookFileBrowserAPIView(APIView):
|
|||||||
new_name = request.data.get('new_name', '')
|
new_name = request.data.get('new_name', '')
|
||||||
|
|
||||||
if file_key in self.protected_files and new_name:
|
if file_key in self.protected_files and new_name:
|
||||||
return Response({'msg': '{} can not be rename'.format(file_key)}, status=status.HTTP_400_BAD_REQUEST)
|
raise JMSException(code='this_file_can_not_be_rename', detail={"msg": _("This file can not be rename")})
|
||||||
|
|
||||||
if os.path.dirname(file_key) == 'root':
|
if os.path.dirname(file_key) == 'root':
|
||||||
file_key = os.path.basename(file_key)
|
file_key = os.path.basename(file_key)
|
||||||
@@ -166,16 +174,20 @@ class PlaybookFileBrowserAPIView(APIView):
|
|||||||
|
|
||||||
if not file_key or file_key == 'root':
|
if not file_key or file_key == 'root':
|
||||||
return Response(status=status.HTTP_400_BAD_REQUEST)
|
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||||
file_path = os.path.join(work_path, file_key)
|
file_path = safe_join(work_path, file_key)
|
||||||
|
|
||||||
# rename
|
# rename
|
||||||
if new_name:
|
if new_name:
|
||||||
new_file_path = os.path.join(os.path.dirname(file_path), new_name)
|
try:
|
||||||
if new_file_path == file_path:
|
new_file_path = safe_join(os.path.dirname(file_path), new_name)
|
||||||
return Response(status=status.HTTP_200_OK)
|
if new_file_path == file_path:
|
||||||
if os.path.exists(new_file_path):
|
return Response(status=status.HTTP_200_OK)
|
||||||
return Response({'msg': '{} already exists'.format(new_name)}, status=status.HTTP_400_BAD_REQUEST)
|
if os.path.exists(new_file_path):
|
||||||
os.rename(file_path, new_file_path)
|
raise JMSException(code='file_already exists', detail={"msg": _("File already exists")})
|
||||||
|
os.rename(file_path, new_file_path)
|
||||||
|
except SuspiciousFileOperation:
|
||||||
|
raise JMSException(code='invalid_file_path', detail={"msg": _("Invalid file path")})
|
||||||
|
|
||||||
# edit content
|
# edit content
|
||||||
else:
|
else:
|
||||||
if not is_directory:
|
if not is_directory:
|
||||||
@@ -189,10 +201,11 @@ class PlaybookFileBrowserAPIView(APIView):
|
|||||||
work_path = playbook.work_dir
|
work_path = playbook.work_dir
|
||||||
file_key = request.query_params.get('key', '')
|
file_key = request.query_params.get('key', '')
|
||||||
if not file_key:
|
if not file_key:
|
||||||
return Response({'msg': 'key is required'}, status=status.HTTP_400_BAD_REQUEST)
|
raise JMSException(code='file_key_is_required', detail={"msg": _("File key is required")})
|
||||||
|
|
||||||
if file_key in self.protected_files:
|
if file_key in self.protected_files:
|
||||||
return Response({'msg': ' {} can not be delete'.format(file_key)}, status=status.HTTP_400_BAD_REQUEST)
|
raise JMSException(code='This file can not be delete', detail={"msg": _("This file can not be delete")})
|
||||||
file_path = os.path.join(work_path, file_key)
|
file_path = safe_join(work_path, file_key)
|
||||||
if os.path.isdir(file_path):
|
if os.path.isdir(file_path):
|
||||||
shutil.rmtree(file_path)
|
shutil.rmtree(file_path)
|
||||||
else:
|
else:
|
||||||
@@ -216,11 +229,12 @@ class PlaybookFileBrowserAPIView(APIView):
|
|||||||
|
|
||||||
relative_path = os.path.relpath(path, root_path)
|
relative_path = os.path.relpath(path, root_path)
|
||||||
for d in dirs:
|
for d in dirs:
|
||||||
|
dir_id = os.path.relpath(safe_join(path, d), root_path)
|
||||||
|
|
||||||
node = {
|
node = {
|
||||||
"name": d,
|
"name": d,
|
||||||
"title": d,
|
"title": d,
|
||||||
"id": os.path.join(relative_path, d) if not os.path.join(relative_path, d).startswith(
|
"id": dir_id,
|
||||||
'.') else d,
|
|
||||||
"isParent": True,
|
"isParent": True,
|
||||||
"open": True,
|
"open": True,
|
||||||
"pId": relative_path if not relative_path.startswith('.') else 'root',
|
"pId": relative_path if not relative_path.startswith('.') else 'root',
|
||||||
@@ -230,12 +244,12 @@ class PlaybookFileBrowserAPIView(APIView):
|
|||||||
node['open'] = True
|
node['open'] = True
|
||||||
nodes.append(node)
|
nodes.append(node)
|
||||||
for f in files:
|
for f in files:
|
||||||
|
file_id = os.path.relpath(safe_join(path, f), root_path)
|
||||||
node = {
|
node = {
|
||||||
"name": f,
|
"name": f,
|
||||||
"title": f,
|
"title": f,
|
||||||
"iconSkin": 'file',
|
"iconSkin": 'file',
|
||||||
"id": os.path.join(relative_path, f) if not os.path.join(relative_path, f).startswith(
|
"id": file_id,
|
||||||
'.') else f,
|
|
||||||
"isParent": False,
|
"isParent": False,
|
||||||
"open": False,
|
"open": False,
|
||||||
"pId": relative_path if not relative_path.startswith('.') else 'root',
|
"pId": relative_path if not relative_path.startswith('.') else 'root',
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
#
|
#
|
||||||
import base64
|
import base64
|
||||||
import datetime
|
import datetime
|
||||||
import random
|
|
||||||
import string
|
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
@@ -562,8 +560,7 @@ class TokenMixin:
|
|||||||
return self.access_keys.first()
|
return self.access_keys.first()
|
||||||
|
|
||||||
def generate_reset_token(self):
|
def generate_reset_token(self):
|
||||||
letter = string.ascii_letters + string.digits
|
token = random_string(50)
|
||||||
token = ''.join([random.choice(letter) for _ in range(50)])
|
|
||||||
self.set_cache(token)
|
self.set_cache(token)
|
||||||
return token
|
return token
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user