mirror of https://github.com/jumpserver/jumpserver
feat: Add LeakPasswords config
parent
0bdbb6fd84
commit
089a5f50f4
|
@ -12,6 +12,7 @@ from accounts.models import Account, AccountRisk, RiskChoice
|
|||
from assets.automations.base.manager import BaseManager
|
||||
from common.const import ConfirmOrIgnore
|
||||
from common.decorators import bulk_create_decorator, bulk_update_decorator
|
||||
from settings.models import LeakPasswords
|
||||
|
||||
|
||||
@bulk_create_decorator(AccountRisk)
|
||||
|
@ -157,10 +158,8 @@ class CheckLeakHandler(BaseCheckHandler):
|
|||
if not account.secret:
|
||||
return False
|
||||
|
||||
sql = 'SELECT 1 FROM passwords WHERE password = ? LIMIT 1'
|
||||
self.cursor.execute(sql, (account.secret,))
|
||||
leak = self.cursor.fetchone() is not None
|
||||
return leak
|
||||
is_exist = LeakPasswords.objects.using('sqlite').filter(password=account.secret).exists()
|
||||
return is_exist
|
||||
|
||||
def clean(self):
|
||||
self.cursor.close()
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 4.1.13 on 2025-05-06 10:23
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0006_alter_accountrisk_username_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='account',
|
||||
name='connectivity',
|
||||
field=models.CharField(choices=[('-', 'Unknown'), ('na', 'N/A'), ('ok', 'OK'), ('err', 'Error'), ('auth_err', 'Authentication error'), ('sudo_err', 'Sudo permission error'), ('password_err', 'Invalid password error'), ('openssh_key_err', 'OpenSSH key error'), ('ntlm_err', 'NTLM credentials rejected error'), ('create_dir_err', 'Create directory error')], default='-', max_length=16, verbose_name='Connectivity'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 4.1.13 on 2025-05-06 10:23
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0018_rename_domain_zone'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='connectivity',
|
||||
field=models.CharField(choices=[('-', 'Unknown'), ('na', 'N/A'), ('ok', 'OK'), ('err', 'Error'), ('auth_err', 'Authentication error'), ('sudo_err', 'Sudo permission error'), ('password_err', 'Invalid password error'), ('openssh_key_err', 'OpenSSH key error'), ('ntlm_err', 'NTLM credentials rejected error'), ('create_dir_err', 'Create directory error')], default='-', max_length=16, verbose_name='Connectivity'),
|
||||
),
|
||||
]
|
|
@ -187,7 +187,7 @@ def on_django_start_set_operate_log_monitor_models(sender, **kwargs):
|
|||
'PermedAsset', 'PermedAccount', 'MenuPermission',
|
||||
'Permission', 'TicketSession', 'ApplyLoginTicket',
|
||||
'ApplyCommandTicket', 'ApplyLoginAssetTicket',
|
||||
'FavoriteAsset', 'ChangeSecretRecord', 'AppProvider', 'Variable'
|
||||
'FavoriteAsset', 'ChangeSecretRecord', 'AppProvider', 'Variable', 'LeakPasswords'
|
||||
}
|
||||
include_models = {'UserSession'}
|
||||
for i, app in enumerate(apps.get_models(), 1):
|
||||
|
|
|
@ -323,7 +323,7 @@ class AuthPostCheckMixin:
|
|||
def _check_passwd_is_too_simple(cls, user: User, password):
|
||||
if not user.is_auth_backend_model():
|
||||
return
|
||||
if user.check_passwd_too_simple(password):
|
||||
if user.check_passwd_too_simple(password) or user.check_leak_password(password):
|
||||
message = _('Your password is too simple, please change it for security')
|
||||
url = cls.generate_reset_password_url_with_flash_msg(user, message=message)
|
||||
raise errors.PasswordTooSimple(url)
|
||||
|
|
|
@ -1548,5 +1548,6 @@
|
|||
"currentTime": "当前时间",
|
||||
"assetId": "资产 ID",
|
||||
"assetName": "资产名称",
|
||||
"assetAddress": "资产地址"
|
||||
"assetAddress": "资产地址",
|
||||
"LeakPasswordList": "弱密码列表"
|
||||
}
|
||||
|
|
|
@ -705,7 +705,7 @@ class Config(dict):
|
|||
'FILE_UPLOAD_SIZE_LIMIT_MB': 200,
|
||||
|
||||
'TICKET_APPLY_ASSET_SCOPE': 'all',
|
||||
'LEAK_PASSWORD_DB_PATH': os.path.join(PROJECT_DIR, 'data', 'leak_password.db'),
|
||||
'LEAK_PASSWORD_DB_PATH': os.path.join(PROJECT_DIR, 'data', 'leak_passwords.db'),
|
||||
|
||||
# Ansible Receptor
|
||||
'RECEPTOR_ENABLED': False,
|
||||
|
|
|
@ -4,9 +4,11 @@ from django.conf import settings
|
|||
from django.core.cache import cache
|
||||
from rest_framework.generics import ListAPIView, CreateAPIView
|
||||
from rest_framework.views import Response
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from users.utils import LoginIpBlockUtil
|
||||
from ..serializers import SecurityBlockIPSerializer
|
||||
from ..models import LeakPasswords
|
||||
from ..serializers import SecurityBlockIPSerializer, LeakPasswordPSerializer
|
||||
|
||||
|
||||
class BlockIPSecurityAPI(ListAPIView):
|
||||
|
@ -56,3 +58,16 @@ class UnlockIPSecurityAPI(CreateAPIView):
|
|||
for ip in ips:
|
||||
LoginIpBlockUtil(ip).clean_block_if_need()
|
||||
return Response(status=200)
|
||||
|
||||
|
||||
class LeakPasswordViewSet(ModelViewSet):
|
||||
serializer_class = LeakPasswordPSerializer
|
||||
model = LeakPasswords
|
||||
rbac_perms = {
|
||||
'*': 'settings.change_security'
|
||||
}
|
||||
queryset = LeakPasswords.objects.none()
|
||||
search_fields = ['password']
|
||||
|
||||
def get_queryset(self):
|
||||
return LeakPasswords.objects.using('sqlite').all()
|
||||
|
|
|
@ -9,3 +9,9 @@ class SettingsConfig(AppConfig):
|
|||
def ready(self):
|
||||
from . import signal_handlers # noqa
|
||||
from . import tasks # noqa
|
||||
from .models import init_sqlite_db, register_sqlite_connection
|
||||
try:
|
||||
init_sqlite_db()
|
||||
register_sqlite_connection()
|
||||
except Exception:
|
||||
pass
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 4.1.13 on 2025-05-06 10:58
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('settings', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='LeakPasswords',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('password', models.CharField(max_length=1024, verbose_name='Password')),
|
||||
],
|
||||
options={
|
||||
'db_table': 'passwords',
|
||||
'managed': False,
|
||||
},
|
||||
),
|
||||
]
|
|
@ -1,10 +1,12 @@
|
|||
import json
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.files.storage import default_storage
|
||||
from django.core.files.uploadedfile import InMemoryUploadedFile
|
||||
from django.db import models
|
||||
from django.db import models, connections
|
||||
from django.db.utils import ProgrammingError, OperationalError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.utils.encoders import JSONEncoder
|
||||
|
@ -208,3 +210,38 @@ def get_chatai_data():
|
|||
data['model'] = settings.DEEPSEEK_MODEL
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def init_sqlite_db():
|
||||
db_path = settings.LEAK_PASSWORD_DB_PATH
|
||||
if not os.path.isfile(db_path):
|
||||
db_path = settings.LEAK_PASSWORD_DB_PATH
|
||||
src = os.path.join(
|
||||
settings.APPS_DIR, 'accounts', 'automations',
|
||||
'check_account', 'leak_passwords.db'
|
||||
)
|
||||
shutil.copy(src, db_path)
|
||||
logger.info(f'init sqlite db {db_path}')
|
||||
return db_path
|
||||
|
||||
|
||||
def register_sqlite_connection():
|
||||
connections.databases['sqlite'] = {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'ATOMIC_REQUESTS': False,
|
||||
'NAME': settings.LEAK_PASSWORD_DB_PATH,
|
||||
'TIME_ZONE': None,
|
||||
'CONN_HEALTH_CHECKS': False,
|
||||
'CONN_MAX_AGE': 0,
|
||||
'OPTIONS': {},
|
||||
'AUTOCOMMIT': True,
|
||||
}
|
||||
|
||||
|
||||
class LeakPasswords(models.Model):
|
||||
id = models.AutoField(primary_key=True)
|
||||
password = models.CharField(max_length=1024, verbose_name=_("Password"))
|
||||
|
||||
class Meta:
|
||||
db_table = 'passwords'
|
||||
managed = False
|
||||
|
|
|
@ -7,9 +7,11 @@ __all__ = [
|
|||
'SecurityPasswordRuleSerializer', 'SecuritySessionSerializer',
|
||||
'SecurityAuthSerializer', 'SecuritySettingSerializer',
|
||||
'SecurityLoginLimitSerializer', 'SecurityBasicSerializer',
|
||||
'SecurityBlockIPSerializer'
|
||||
'SecurityBlockIPSerializer', 'LeakPasswordPSerializer'
|
||||
]
|
||||
|
||||
from settings.models import LeakPasswords
|
||||
|
||||
|
||||
class SecurityPasswordRuleSerializer(serializers.Serializer):
|
||||
SECURITY_PASSWORD_EXPIRATION_TIME = serializers.IntegerField(
|
||||
|
@ -269,3 +271,20 @@ class SecuritySettingSerializer(
|
|||
class SecurityBlockIPSerializer(serializers.Serializer):
|
||||
id = serializers.UUIDField(required=False)
|
||||
ip = serializers.CharField(max_length=1024, required=False, allow_blank=True)
|
||||
|
||||
|
||||
class LeakPasswordPSerializer(serializers.ModelSerializer):
|
||||
|
||||
def create(self, validated_data):
|
||||
return LeakPasswords.objects.using('sqlite').create(**validated_data)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
for attr, value in validated_data.items():
|
||||
setattr(instance, attr, value)
|
||||
instance.save(using='sqlite')
|
||||
return instance
|
||||
|
||||
class Meta:
|
||||
read_only_fields = ['id']
|
||||
fields = read_only_fields + ['password']
|
||||
model = LeakPasswords
|
||||
|
|
|
@ -8,6 +8,7 @@ from .. import api
|
|||
app_name = 'common'
|
||||
router = BulkRouter()
|
||||
router.register(r'chatai-prompts', api.ChatPromptViewSet, 'chatai-prompt')
|
||||
router.register(r'leak-passwords', api.LeakPasswordViewSet, 'leak-passwords')
|
||||
|
||||
urlpatterns = [
|
||||
path('mail/testing/', api.MailTestingAPI.as_view(), name='mail-testing'),
|
||||
|
|
|
@ -17,6 +17,7 @@ from common.utils import (
|
|||
get_logger,
|
||||
lazyproperty,
|
||||
)
|
||||
from settings.models import LeakPasswords
|
||||
from users.signals import post_user_change_password
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
@ -298,3 +299,8 @@ class AuthMixin:
|
|||
return ""
|
||||
password = signer.unsign(secret)
|
||||
return password
|
||||
|
||||
@staticmethod
|
||||
def check_leak_password(password):
|
||||
is_exist = LeakPasswords.objects.using('sqlite').filter(password=password).exists()
|
||||
return is_exist
|
||||
|
|
|
@ -16,8 +16,8 @@ from authentication.utils import check_user_property_is_correct
|
|||
from common.const.choices import COUNTRY_CALLING_CODES
|
||||
from common.utils import FlashMessageUtil, get_object_or_none, random_string
|
||||
from common.utils.verify_code import SendAndVerifyCodeUtil
|
||||
from users.serializers import SmsUserSerializer
|
||||
from users.notifications import ResetPasswordSuccessMsg
|
||||
from users.serializers import SmsUserSerializer
|
||||
from ... import forms
|
||||
from ...models import User
|
||||
from ...utils import check_password_rules, get_password_check_rules
|
||||
|
@ -220,6 +220,11 @@ class UserResetPasswordView(FormView):
|
|||
form.add_error('new_password', error)
|
||||
return self.form_invalid(form)
|
||||
|
||||
if user.check_leak_password(password):
|
||||
error = _('Your password is too simple, please change it for security')
|
||||
form.add_error('new_password', error)
|
||||
return self.form_invalid(form)
|
||||
|
||||
user.reset_password(password)
|
||||
User.expired_reset_password_token(token)
|
||||
|
||||
|
|
Loading…
Reference in New Issue