Compare commits

...

19 Commits

Author SHA1 Message Date
ibuler
3b30e329ab fix: 修改可能迁移的问题 2023-11-01 03:11:20 -05:00
ibuler
3dc853c7f2 perf: 优化选择应用发布机 2023-10-30 16:15:37 +08:00
fit2bot
932aed97d3 fix: 账号批量更新失败 (#11786)
Co-authored-by: feng <1304903146@qq.com>
2023-10-10 17:24:30 +08:00
feng
c464e95a21 fix: 修复账号批量更新失败问题 2023-10-09 10:05:20 +08:00
jiangweidong
0f0af19d49 perf: 更新jms-storage版本 2023-09-28 18:08:04 +05:00
feng
6a54ff8714 fix: 账号授权过滤指定账号api 失效问题 2023-09-27 13:46:35 +05:00
feng
1e0489bb96 fix: 账号授权过滤指定账号api 失效问题 2023-09-27 13:46:35 +05:00
ibuler
980ddcd833 perf: 优化发送邮件 2023-09-27 08:26:48 +05:00
feng
1ec2cd6087 perf: 账号模版 生成随机密码密钥及账号批量更新500 2023-09-26 12:55:29 +08:00
Bai
257ef464f7 fix: 修复未生成的迁移文件 2023-09-26 12:01:45 +08:00
ibuler
9cd3cc1da0 fix: pubkey auth require svc sign 2023-09-25 23:30:46 +08:00
ibuler
9520a23f4c fix: 修复暴力校验验证码 2023-09-25 23:04:53 +08:00
fit2bot
a4ff9dace3 fix: 修复用户username 中文 登录失败问题 (#11693)
Co-authored-by: feng <1304903146@qq.com>
2023-09-25 21:38:56 +08:00
jiangweidong
7a1214f358 perf: 优化找回密码时区号带加号无法匹配的问题 2023-09-25 16:41:28 +08:00
Bai
2fdc4c613f fix: 修复系统用户同步同时包含pwd/ssh-key导致创建账号id冲突报错的问题 2023-09-25 16:23:22 +08:00
吴小白
df6525933a perf: 添加 ping 命令 2023-09-25 10:50:14 +08:00
吴小白
6aef27c824 perf: 添加 patch 命令 2023-09-22 15:20:25 +08:00
Bai
26c3409d84 fix: 解决节点资产数量方法计算不准确的问题 2023-09-22 15:18:44 +08:00
Bryan
3c54c82ce9 Merge pull request #11636 from jumpserver/dev
v3.7.0
2023-09-21 17:02:48 +08:00
19 changed files with 101 additions and 48 deletions

View File

@@ -36,9 +36,11 @@ ARG TOOLS=" \
curl \
default-libmysqlclient-dev \
default-mysql-client \
iputils-ping \
locales \
nmap \
openssh-client \
patch \
sshpass \
telnet \
vim \

View File

@@ -6,8 +6,8 @@ from rest_framework.status import HTTP_200_OK
from accounts import serializers
from accounts.filters import AccountFilterSet
from accounts.models import Account
from accounts.mixins import AccountRecordViewLogMixin
from accounts.models import Account
from assets.models import Asset, Node
from common.api.mixin import ExtraFilterFieldsMixin
from common.permissions import UserConfirmation, ConfirmType, IsValidUser
@@ -57,19 +57,19 @@ class AccountViewSet(OrgBulkModelViewSet):
permission_classes=[IsValidUser]
)
def username_suggestions(self, request, *args, **kwargs):
asset_ids = request.data.get('assets')
node_ids = request.data.get('nodes')
username = request.data.get('username')
asset_ids = request.data.get('assets', [])
node_ids = request.data.get('nodes', [])
username = request.data.get('username', '')
assets = Asset.objects.all()
if asset_ids:
assets = assets.filter(id__in=asset_ids)
accounts = Account.objects.all()
if node_ids:
nodes = Node.objects.filter(id__in=node_ids)
node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True)
assets = assets.filter(id__in=set(list(asset_ids) + list(node_asset_ids)))
asset_ids.extend(node_asset_ids)
if asset_ids:
accounts = accounts.filter(asset_id__in=list(set(asset_ids)))
accounts = Account.objects.filter(asset__in=assets)
if username:
accounts = accounts.filter(username__icontains=username)
usernames = list(accounts.values_list('username', flat=True).distinct()[:10])

View File

@@ -13,11 +13,11 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='changesecretautomation',
name='secret_strategy',
field=models.CharField(choices=[('specific', 'Specific password'), ('random', 'Random')], default='specific', max_length=16, verbose_name='Secret strategy'),
field=models.CharField(choices=[('specific', 'Specific secret'), ('random', 'Random generate')], default='specific', max_length=16, verbose_name='Secret strategy'),
),
migrations.AlterField(
model_name='pushaccountautomation',
name='secret_strategy',
field=models.CharField(choices=[('specific', 'Specific password'), ('random', 'Random')], default='specific', max_length=16, verbose_name='Secret strategy'),
field=models.CharField(choices=[('specific', 'Specific secret'), ('random', 'Random generate')], default='specific', max_length=16, verbose_name='Secret strategy'),
),
]

View File

@@ -29,6 +29,6 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='accounttemplate',
name='secret_strategy',
field=models.CharField(choices=[('specific', 'Specific password'), ('random', 'Random')], default='specific', max_length=16, verbose_name='Secret strategy'),
field=models.CharField(choices=[('specific', 'Specific secret'), ('random', 'Random generate')], default='specific', max_length=16, verbose_name='Secret strategy'),
),
]

View File

@@ -37,8 +37,9 @@ class VaultManagerMixin(models.Manager):
post_save.send(obj.__class__, instance=obj, created=True)
return objs
def bulk_update(self, objs, batch_size=None, ignore_conflicts=False):
objs = super().bulk_update(objs, batch_size=batch_size, ignore_conflicts=ignore_conflicts)
def bulk_update(self, objs, fields, batch_size=None):
fields = ["_secret" if field == "secret" else field for field in fields]
super().bulk_update(objs, fields, batch_size=batch_size)
for obj in objs:
post_save.send(obj.__class__, instance=obj, created=False)
return objs

View File

@@ -1,7 +1,9 @@
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from accounts.const import SecretStrategy, SecretType
from accounts.models import AccountTemplate, Account
from accounts.utils import SecretGenerator
from common.serializers import SecretReadableMixin
from common.serializers.fields import ObjectRelatedField
from .base import BaseAccountSerializer
@@ -55,9 +57,20 @@ class AccountTemplateSerializer(BaseAccountSerializer):
accounts = Account.objects.filter(**query_data)
instance.bulk_sync_account_secret(accounts, self.context['request'].user.id)
@staticmethod
def generate_secret(attrs):
secret_type = attrs.get('secret_type', SecretType.PASSWORD)
secret_strategy = attrs.get('secret_strategy', SecretStrategy.custom)
password_rules = attrs.get('password_rules')
if secret_strategy != SecretStrategy.random:
return
generator = SecretGenerator(secret_strategy, secret_type, password_rules)
attrs['secret'] = generator.get_secret()
def validate(self, attrs):
self._is_sync_account = attrs.pop('is_sync_account', None)
attrs = super().validate(attrs)
self.generate_secret(attrs)
return attrs
def update(self, instance, validated_data):

View File

@@ -107,8 +107,9 @@ def create_app_nodes(apps, org_id):
'key': next_key, 'value': name, 'parent_key': parent_key,
'full_value': full_value, 'org_id': org_id
}
node, created = node_model.objects.get_or_create(
node, __ = node_model.objects.get_or_create(
defaults=defaults, value=name, org_id=org_id,
parent_key=parent_key
)
node.parent = parent
return node

View File

@@ -25,7 +25,7 @@ def migrate_asset_accounts(apps, schema_editor):
count += len(auth_books)
# auth book 和 account 相同的属性
same_attrs = [
'id', 'username', 'comment', 'date_created', 'date_updated',
'username', 'comment', 'date_created', 'date_updated',
'created_by', 'asset_id', 'org_id',
]
# 认证的属性,可能是 auth_book 的,可能是 system_user 的

View File

@@ -402,12 +402,7 @@ class NodeAssetsMixin(NodeAllAssetsMappingMixin):
return Asset.objects.filter(q).distinct()
def get_assets_amount(self):
q = Q(node__key__startswith=f'{self.key}:') | Q(node__key=self.key)
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()
return self.get_all_assets().count()
@classmethod
def get_node_all_assets_by_key_v2(cls, key):

View File

@@ -1,8 +1,9 @@
# -*- coding: utf-8 -*-
#
from django.contrib.auth import get_user_model
from django.conf import settings
from django.contrib.auth import get_user_model
from common.permissions import ServiceAccountSignaturePermission
from .base import JMSBaseAuthBackend
UserModel = get_user_model()
@@ -18,6 +19,10 @@ class PublicKeyAuthBackend(JMSBaseAuthBackend):
def authenticate(self, request, username=None, public_key=None, **kwargs):
if not public_key:
return None
permission = ServiceAccountSignaturePermission()
if not permission.has_permission(request, None):
return None
if username is None:
username = kwargs.get(UserModel.USERNAME_FIELD)
try:
@@ -26,7 +31,7 @@ class PublicKeyAuthBackend(JMSBaseAuthBackend):
return None
else:
if user.check_public_key(public_key) and \
self.user_can_authenticate(user):
self.user_can_authenticate(user):
return user
def get_user(self, user_id):

View File

@@ -310,12 +310,6 @@ class UserLoginGuardView(mixins.AuthMixin, RedirectView):
age = self.request.session.get_expiry_age()
self.request.session.set_expiry(age)
def get(self, request, *args, **kwargs):
response = super().get(request, *args, **kwargs)
if request.user.is_authenticated:
response.set_cookie('jms_username', request.user.username)
return response
def get_redirect_url(self, *args, **kwargs):
try:
user = self.get_user_from_session()

View File

@@ -86,3 +86,38 @@ class UserConfirmation(permissions.BasePermission):
min_level = ConfirmType.values.index(confirm_type) + 1
name = 'UserConfirmationLevel{}TTL{}'.format(min_level, ttl)
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

View File

@@ -36,7 +36,9 @@ def send_mail_async(*args, **kwargs):
args[0] = (settings.EMAIL_SUBJECT_PREFIX or '') + args[0]
from_email = settings.EMAIL_FROM or settings.EMAIL_HOST_USER
args.insert(2, from_email)
args = tuple(args)
args[3] = [mail for mail in args[3] if mail != 'admin@mycomany.com']
args = tuple(args)
try:
return send_mail(*args, **kwargs)
@@ -50,6 +52,7 @@ def send_mail_attachment_async(subject, message, recipient_list, attachment_list
attachment_list = []
from_email = settings.EMAIL_FROM or settings.EMAIL_HOST_USER
subject = (settings.EMAIL_SUBJECT_PREFIX or '') + subject
recipient_list = [mail for mail in recipient_list if mail != 'admin@mycomany.com']
email = EmailMultiAlternatives(
subject=subject,
body=message,

View File

@@ -26,6 +26,7 @@ class SendAndVerifyCodeUtil(object):
self.target = target
self.backend = backend
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.other_args = kwargs
@@ -47,6 +48,11 @@ class SendAndVerifyCodeUtil(object):
raise
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)
if not right:
raise CodeExpired
@@ -59,6 +65,7 @@ class SendAndVerifyCodeUtil(object):
def __clear(self):
cache.delete(self.key)
cache.delete(self.verify_key)
def __ttl(self):
return cache.ttl(self.key)

View File

@@ -12,7 +12,6 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='applet',
name='edition',
field=models.CharField(choices=[('community', 'Community'), ('enterprise', 'Enterprise')],
default='community', max_length=128, verbose_name='Edition'),
field=models.CharField(choices=[('community', 'Community edition'), ('enterprise', 'Enterprise')], default='community', max_length=128, verbose_name='Edition'),
),
]

View File

@@ -162,7 +162,7 @@ class Applet(JMSBaseModel):
for host_id in using_host_ids.values():
counts[host_id] += 1
hosts = list(sorted(hosts, key=lambda h: counts[h.id]))
hosts = list(sorted(hosts, key=lambda h: counts[str(h.id)]))
return hosts[0]
def select_host(self, user, asset):

View File

@@ -114,6 +114,11 @@ class UserForgotPasswordView(FormView):
target = form.cleaned_data[form_type]
code = form.cleaned_data['code']
query_key = form_type
if form_type == 'sms':
query_key = 'phone'
target = target.lstrip('+')
try:
sender_util = SendAndVerifyCodeUtil(target, backend=form_type)
sender_util.verify(code)
@@ -121,7 +126,6 @@ class UserForgotPasswordView(FormView):
form.add_error('code', str(e))
return super().form_invalid(form)
query_key = 'phone' if form_type == 'sms' else form_type
user = get_object_or_none(User, **{'username': username, query_key: target})
if not user:
form.add_error('code', _('No user matched'))

18
poetry.lock generated
View File

@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
[[package]]
name = "adal"
@@ -2655,14 +2655,8 @@ files = [
[package.dependencies]
google-auth = ">=2.14.1,<3.0.dev0"
googleapis-common-protos = ">=1.56.2,<2.0.dev0"
grpcio = [
{version = ">=1.33.2,<2.0dev", optional = true, markers = "extra == \"grpc\""},
{version = ">=1.49.1,<2.0dev", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""},
]
grpcio-status = [
{version = ">=1.33.2,<2.0.dev0", optional = true, markers = "extra == \"grpc\""},
{version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""},
]
grpcio = {version = ">=1.49.1,<2.0dev", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}
grpcio-status = {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}
protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0.dev0"
requests = ">=2.18.0,<3.0.0.dev0"
@@ -3345,12 +3339,12 @@ reference = "tsinghua"
[[package]]
name = "jms-storage"
version = "0.0.51"
version = "0.0.52"
description = "Jumpserver storage python sdk tools"
optional = false
python-versions = "*"
files = [
{file = "jms-storage-0.0.51.tar.gz", hash = "sha256:47a50ac4d952a21693b0e2f926f42fa0d02bc1fa8e507a8284059743b2b81911"},
{file = "jms-storage-0.0.52.tar.gz", hash = "sha256:15303281a1d1a3ac24a5a9fb0d78abda3aa1f752590aab867923647a485ccfbd"},
]
[package.dependencies]
@@ -7276,4 +7270,4 @@ reference = "tsinghua"
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "b7d8e793f247e91e1bd22404559ed495e619fe691fa92712cacf7bd146c0eb8f"
content-hash = "bf72acdbac5e62c239033fd629835ad30788a3fcb07ac9a2dc2f15f321da6c30"

View File

@@ -47,7 +47,7 @@ pynacl = "1.5.0"
python-dateutil = "2.8.2"
pyyaml = "6.0.1"
requests = "2.31.0"
jms-storage = "0.0.51"
jms-storage = "0.0.52"
simplejson = "3.19.1"
six = "1.16.0"
sshtunnel = "0.4.0"