Compare commits

...

11 Commits
v4.4 ... v3.7.1

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
14 changed files with 78 additions and 23 deletions

View File

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

1
GITSHA Normal file
View File

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

View File

@@ -13,11 +13,11 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='changesecretautomation', model_name='changesecretautomation',
name='secret_strategy', 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( migrations.AlterField(
model_name='pushaccountautomation', model_name='pushaccountautomation',
name='secret_strategy', 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( migrations.AddField(
model_name='accounttemplate', model_name='accounttemplate',
name='secret_strategy', 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) post_save.send(obj.__class__, instance=obj, created=True)
return objs return objs
def bulk_update(self, objs, batch_size=None, ignore_conflicts=False): def bulk_update(self, objs, fields, batch_size=None):
objs = super().bulk_update(objs, batch_size=batch_size, ignore_conflicts=ignore_conflicts) objs = super().bulk_update(objs, fields, batch_size=batch_size)
for obj in objs: for obj in objs:
post_save.send(obj.__class__, instance=obj, created=False) post_save.send(obj.__class__, instance=obj, created=False)
return objs return objs

View File

@@ -1,7 +1,9 @@
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from accounts.const import SecretStrategy, SecretType
from accounts.models import AccountTemplate, Account from accounts.models import AccountTemplate, Account
from accounts.utils import SecretGenerator
from common.serializers import SecretReadableMixin from common.serializers import SecretReadableMixin
from common.serializers.fields import ObjectRelatedField from common.serializers.fields import ObjectRelatedField
from .base import BaseAccountSerializer from .base import BaseAccountSerializer
@@ -55,9 +57,20 @@ class AccountTemplateSerializer(BaseAccountSerializer):
accounts = Account.objects.filter(**query_data) accounts = Account.objects.filter(**query_data)
instance.bulk_sync_account_secret(accounts, self.context['request'].user.id) 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): def validate(self, attrs):
self._is_sync_account = attrs.pop('is_sync_account', None) self._is_sync_account = attrs.pop('is_sync_account', None)
attrs = super().validate(attrs) attrs = super().validate(attrs)
self.generate_secret(attrs)
return attrs return attrs
def update(self, instance, validated_data): def update(self, instance, validated_data):

View File

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

View File

@@ -402,12 +402,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):

View File

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

View File

@@ -310,12 +310,6 @@ class UserLoginGuardView(mixins.AuthMixin, RedirectView):
age = self.request.session.get_expiry_age() age = self.request.session.get_expiry_age()
self.request.session.set_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): def get_redirect_url(self, *args, **kwargs):
try: try:
user = self.get_user_from_session() user = self.get_user_from_session()

View File

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

View File

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

View File

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

View File

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