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 assets.automations.base.manager import BaseManager
|
||||||
from common.const import ConfirmOrIgnore
|
from common.const import ConfirmOrIgnore
|
||||||
from common.decorators import bulk_create_decorator, bulk_update_decorator
|
from common.decorators import bulk_create_decorator, bulk_update_decorator
|
||||||
|
from settings.models import LeakPasswords
|
||||||
|
|
||||||
|
|
||||||
@bulk_create_decorator(AccountRisk)
|
@bulk_create_decorator(AccountRisk)
|
||||||
|
@ -157,10 +158,8 @@ class CheckLeakHandler(BaseCheckHandler):
|
||||||
if not account.secret:
|
if not account.secret:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
sql = 'SELECT 1 FROM passwords WHERE password = ? LIMIT 1'
|
is_exist = LeakPasswords.objects.using('sqlite').filter(password=account.secret).exists()
|
||||||
self.cursor.execute(sql, (account.secret,))
|
return is_exist
|
||||||
leak = self.cursor.fetchone() is not None
|
|
||||||
return leak
|
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
self.cursor.close()
|
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',
|
'PermedAsset', 'PermedAccount', 'MenuPermission',
|
||||||
'Permission', 'TicketSession', 'ApplyLoginTicket',
|
'Permission', 'TicketSession', 'ApplyLoginTicket',
|
||||||
'ApplyCommandTicket', 'ApplyLoginAssetTicket',
|
'ApplyCommandTicket', 'ApplyLoginAssetTicket',
|
||||||
'FavoriteAsset', 'ChangeSecretRecord', 'AppProvider', 'Variable'
|
'FavoriteAsset', 'ChangeSecretRecord', 'AppProvider', 'Variable', 'LeakPasswords'
|
||||||
}
|
}
|
||||||
include_models = {'UserSession'}
|
include_models = {'UserSession'}
|
||||||
for i, app in enumerate(apps.get_models(), 1):
|
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):
|
def _check_passwd_is_too_simple(cls, user: User, password):
|
||||||
if not user.is_auth_backend_model():
|
if not user.is_auth_backend_model():
|
||||||
return
|
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')
|
message = _('Your password is too simple, please change it for security')
|
||||||
url = cls.generate_reset_password_url_with_flash_msg(user, message=message)
|
url = cls.generate_reset_password_url_with_flash_msg(user, message=message)
|
||||||
raise errors.PasswordTooSimple(url)
|
raise errors.PasswordTooSimple(url)
|
||||||
|
|
|
@ -1548,5 +1548,6 @@
|
||||||
"currentTime": "当前时间",
|
"currentTime": "当前时间",
|
||||||
"assetId": "资产 ID",
|
"assetId": "资产 ID",
|
||||||
"assetName": "资产名称",
|
"assetName": "资产名称",
|
||||||
"assetAddress": "资产地址"
|
"assetAddress": "资产地址",
|
||||||
|
"LeakPasswordList": "弱密码列表"
|
||||||
}
|
}
|
||||||
|
|
|
@ -705,7 +705,7 @@ class Config(dict):
|
||||||
'FILE_UPLOAD_SIZE_LIMIT_MB': 200,
|
'FILE_UPLOAD_SIZE_LIMIT_MB': 200,
|
||||||
|
|
||||||
'TICKET_APPLY_ASSET_SCOPE': 'all',
|
'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
|
# Ansible Receptor
|
||||||
'RECEPTOR_ENABLED': False,
|
'RECEPTOR_ENABLED': False,
|
||||||
|
|
|
@ -4,9 +4,11 @@ from django.conf import settings
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from rest_framework.generics import ListAPIView, CreateAPIView
|
from rest_framework.generics import ListAPIView, CreateAPIView
|
||||||
from rest_framework.views import Response
|
from rest_framework.views import Response
|
||||||
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from users.utils import LoginIpBlockUtil
|
from users.utils import LoginIpBlockUtil
|
||||||
from ..serializers import SecurityBlockIPSerializer
|
from ..models import LeakPasswords
|
||||||
|
from ..serializers import SecurityBlockIPSerializer, LeakPasswordPSerializer
|
||||||
|
|
||||||
|
|
||||||
class BlockIPSecurityAPI(ListAPIView):
|
class BlockIPSecurityAPI(ListAPIView):
|
||||||
|
@ -56,3 +58,16 @@ class UnlockIPSecurityAPI(CreateAPIView):
|
||||||
for ip in ips:
|
for ip in ips:
|
||||||
LoginIpBlockUtil(ip).clean_block_if_need()
|
LoginIpBlockUtil(ip).clean_block_if_need()
|
||||||
return Response(status=200)
|
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):
|
def ready(self):
|
||||||
from . import signal_handlers # noqa
|
from . import signal_handlers # noqa
|
||||||
from . import tasks # 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 json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.core.files.storage import default_storage
|
from django.core.files.storage import default_storage
|
||||||
from django.core.files.uploadedfile import InMemoryUploadedFile
|
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.db.utils import ProgrammingError, OperationalError
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework.utils.encoders import JSONEncoder
|
from rest_framework.utils.encoders import JSONEncoder
|
||||||
|
@ -208,3 +210,38 @@ def get_chatai_data():
|
||||||
data['model'] = settings.DEEPSEEK_MODEL
|
data['model'] = settings.DEEPSEEK_MODEL
|
||||||
|
|
||||||
return data
|
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',
|
'SecurityPasswordRuleSerializer', 'SecuritySessionSerializer',
|
||||||
'SecurityAuthSerializer', 'SecuritySettingSerializer',
|
'SecurityAuthSerializer', 'SecuritySettingSerializer',
|
||||||
'SecurityLoginLimitSerializer', 'SecurityBasicSerializer',
|
'SecurityLoginLimitSerializer', 'SecurityBasicSerializer',
|
||||||
'SecurityBlockIPSerializer'
|
'SecurityBlockIPSerializer', 'LeakPasswordPSerializer'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
from settings.models import LeakPasswords
|
||||||
|
|
||||||
|
|
||||||
class SecurityPasswordRuleSerializer(serializers.Serializer):
|
class SecurityPasswordRuleSerializer(serializers.Serializer):
|
||||||
SECURITY_PASSWORD_EXPIRATION_TIME = serializers.IntegerField(
|
SECURITY_PASSWORD_EXPIRATION_TIME = serializers.IntegerField(
|
||||||
|
@ -269,3 +271,20 @@ class SecuritySettingSerializer(
|
||||||
class SecurityBlockIPSerializer(serializers.Serializer):
|
class SecurityBlockIPSerializer(serializers.Serializer):
|
||||||
id = serializers.UUIDField(required=False)
|
id = serializers.UUIDField(required=False)
|
||||||
ip = serializers.CharField(max_length=1024, required=False, allow_blank=True)
|
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'
|
app_name = 'common'
|
||||||
router = BulkRouter()
|
router = BulkRouter()
|
||||||
router.register(r'chatai-prompts', api.ChatPromptViewSet, 'chatai-prompt')
|
router.register(r'chatai-prompts', api.ChatPromptViewSet, 'chatai-prompt')
|
||||||
|
router.register(r'leak-passwords', api.LeakPasswordViewSet, 'leak-passwords')
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('mail/testing/', api.MailTestingAPI.as_view(), name='mail-testing'),
|
path('mail/testing/', api.MailTestingAPI.as_view(), name='mail-testing'),
|
||||||
|
|
|
@ -17,6 +17,7 @@ from common.utils import (
|
||||||
get_logger,
|
get_logger,
|
||||||
lazyproperty,
|
lazyproperty,
|
||||||
)
|
)
|
||||||
|
from settings.models import LeakPasswords
|
||||||
from users.signals import post_user_change_password
|
from users.signals import post_user_change_password
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
@ -298,3 +299,8 @@ class AuthMixin:
|
||||||
return ""
|
return ""
|
||||||
password = signer.unsign(secret)
|
password = signer.unsign(secret)
|
||||||
return password
|
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.const.choices import COUNTRY_CALLING_CODES
|
||||||
from common.utils import FlashMessageUtil, get_object_or_none, random_string
|
from common.utils import FlashMessageUtil, get_object_or_none, random_string
|
||||||
from common.utils.verify_code import SendAndVerifyCodeUtil
|
from common.utils.verify_code import SendAndVerifyCodeUtil
|
||||||
from users.serializers import SmsUserSerializer
|
|
||||||
from users.notifications import ResetPasswordSuccessMsg
|
from users.notifications import ResetPasswordSuccessMsg
|
||||||
|
from users.serializers import SmsUserSerializer
|
||||||
from ... import forms
|
from ... import forms
|
||||||
from ...models import User
|
from ...models import User
|
||||||
from ...utils import check_password_rules, get_password_check_rules
|
from ...utils import check_password_rules, get_password_check_rules
|
||||||
|
@ -220,6 +220,11 @@ class UserResetPasswordView(FormView):
|
||||||
form.add_error('new_password', error)
|
form.add_error('new_password', error)
|
||||||
return self.form_invalid(form)
|
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.reset_password(password)
|
||||||
User.expired_reset_password_token(token)
|
User.expired_reset_password_token(token)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue