Compare commits

...

12 Commits

Author SHA1 Message Date
fit2bot
e75da94fc1 feat: Update v3.7.1 2023-09-26 13:12:14 +08: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
14 changed files with 78 additions and 23 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 \

1
GITSHA Normal file
View File

@@ -0,0 +1 @@
1ec2cd60873d0035ad23803414d23c3636b40be1

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,8 @@ 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):
objs = 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

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

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

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