Compare commits

...

22 Commits
ccrc_v3 ... v3

Author SHA1 Message Date
fit2bot
a497b3cf94 fix: Add '/media/' to the list of whitelisted URLs for MFA login (#16412)
Co-authored-by: wangruidong <940853815@qq.com>
2025-12-10 14:19:39 +08:00
fit2bot
8548b73063 fix: Failed to switch languages (#16326)
Co-authored-by: wangruidong <940853815@qq.com>
2025-11-24 11:13:56 +08:00
fit2bot
182320f492 fix: SAML2 authentication failure with Okta integration (#16250)
Co-authored-by: wangruidong <940853815@qq.com>
2025-11-07 15:42:09 +08:00
fit2bot
40d326d6a6 fix: Any change to the LDAP server URI should require re-authentication and explicit re-entry of (#16195)
the bind password, not reuse stored credentials

Co-authored-by: wangruidong <940853815@qq.com>
2025-10-23 18:09:46 +08:00
ibuler
b87554f9db perf: conn token get 2025-10-21 11:05:05 +08:00
Bai
3c255f9fa6 fix: AK/SK remained valid after the user expired. 2025-09-16 13:31:53 +08:00
fit2bot
a6b5437f6a fix: Open redirect security vulnerability (#15938)
Co-authored-by: wangruidong <940853815@qq.com>
2025-08-27 11:03:30 +08:00
feng
71c690ef9e perf: Account remove 2025-08-26 17:28:28 +08:00
feng
bee07db900 perf: Windows change secret check_conn_after_change 2025-08-26 14:55:01 +08:00
Bryan
115eb7c15a Reformat import statements in utils.py 2025-08-25 18:20:18 +08:00
Bai
88810263cd perf: test rebase for commit id changed 2 2025-08-25 18:16:15 +08:00
Bai
3d95bc4656 perf: test rebase for commit id changed 2025-08-25 18:05:47 +08:00
feng
69de08bb5d fix: Ticket filtering current reviewer issue 2025-08-25 14:35:15 +08:00
Bai
5c234fdd0c perf: lock xmlsec==1.3.13 2025-08-25 11:19:24 +08:00
fit2bot
be5baa5a3f perf: Update IP group validation to include address validation (#15919)
Co-authored-by: wangruidong <940853815@qq.com>
2025-08-25 11:16:52 +08:00
feng
2f1a65f120 perf: migrate 2025-08-25 11:12:25 +08:00
fit2bot
e6d02eaf4c fix: Add third party login check is block (#15916)
Co-authored-by: wangruidong <940853815@qq.com>
2025-08-25 10:51:42 +08:00
fit2bot
6d6dec2752 fix: Prevent nested resource issues in type nodes tree API (#15915)
Co-authored-by: wangruidong <940853815@qq.com>
2025-08-25 10:40:10 +08:00
feng
c6c067c44b perf: Mongodb ping 2025-08-19 19:09:52 +08:00
feng
84ec1b047a perf: FormatAssetInfo posix_format cpu_model 2025-08-15 16:51:33 +08:00
feng
e6dca2ec14 fix: automation mysql priv and postgresql finally test the connectivity 2025-08-13 15:34:32 +08:00
wangruidong
8793003d18 perf: check_asset_permission_will_expired filter is_active=True 2025-08-07 10:18:24 +08:00
45 changed files with 1936 additions and 1258 deletions

View File

@@ -53,3 +53,5 @@
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
connection_options:
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
register: result
failed_when: not result.is_available

View File

@@ -36,7 +36,8 @@
name: "{{ account.username }}"
password: "{{ account.secret }}"
host: "%"
priv: "{{ account.username + '.*:USAGE' if db_name == '' else db_name + '.*:ALL' }}"
priv: "{{ omit if db_name == '' else db_name + '.*:ALL' }}"
append_privs: "{{ db_name != '' | bool }}"
ignore_errors: true
when: db_info is succeeded

View File

@@ -39,3 +39,5 @@
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
db: "{{ jms_asset.spec_info.db_name }}"
register: result
failed_when: not result.is_available

View File

@@ -28,4 +28,6 @@
vars:
ansible_user: "{{ account.username }}"
ansible_password: "{{ account.secret }}"
when: account.secret_type == "password"
when:
- account.secret_type == "password"
- check_conn_after_change

View File

@@ -31,5 +31,7 @@
login_password: "{{ account.secret }}"
login_secret_type: "{{ account.secret_type }}"
login_private_key_path: "{{ account.private_key_path }}"
when: account.secret_type == "password"
when:
- account.secret_type == "password"
- check_conn_after_change
delegate_to: localhost

View File

@@ -53,3 +53,5 @@
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
connection_options:
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
register: result
failed_when: not result.is_available

View File

@@ -36,7 +36,8 @@
name: "{{ account.username }}"
password: "{{ account.secret }}"
host: "%"
priv: "{{ account.username + '.*:USAGE' if db_name == '' else db_name + '.*:ALL' }}"
priv: "{{ omit if db_name == '' else db_name + '.*:ALL' }}"
append_privs: "{{ db_name != '' | bool }}"
ignore_errors: true
when: db_info is succeeded

View File

@@ -31,7 +31,6 @@
role_attr_flags: LOGIN
ignore_errors: true
when: result is succeeded
register: change_info
- name: Verify password
community.postgresql.postgresql_ping:
@@ -40,8 +39,5 @@
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
db: "{{ jms_asset.spec_info.db_name }}"
when:
- result is succeeded
- change_info is succeeded
register: result
failed_when: not result.is_available

View File

@@ -28,4 +28,6 @@
vars:
ansible_user: "{{ account.username }}"
ansible_password: "{{ account.secret }}"
when: account.secret_type == "password"
when:
- account.secret_type == "password"
- check_conn_after_change

View File

@@ -31,5 +31,7 @@
login_password: "{{ account.secret }}"
login_secret_type: "{{ account.secret_type }}"
login_private_key_path: "{{ account.private_key_path }}"
when: account.secret_type == "password"
when:
- account.secret_type == "password"
- check_conn_after_change
delegate_to: localhost

View File

@@ -16,3 +16,5 @@
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
connection_options:
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert }}"
register: result
failed_when: not result.is_available

View File

@@ -2,10 +2,11 @@ import copy
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from accounts.const import SecretType, DEFAULT_PASSWORD_RULES
from common.utils import ssh_key_gen, random_string
from common.utils import validate_ssh_private_key, parse_ssh_private_key_str
from common.utils import (
validate_ssh_private_key, parse_ssh_private_key_str, ssh_key_gen,
random_string
)
class SecretGenerator:

View File

@@ -1,5 +1,7 @@
# coding: utf-8
#
from urllib.parse import urlparse
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
@@ -8,7 +10,7 @@ from common.utils.ip import is_ip_address, is_ip_network, is_ip_segment
logger = get_logger(__file__)
__all__ = ['RuleSerializer', 'ip_group_child_validator', 'ip_group_help_text']
__all__ = ['RuleSerializer', 'ip_group_child_validator', 'ip_group_help_text', 'address_validator']
def ip_group_child_validator(ip_group_child):
@@ -21,6 +23,19 @@ def ip_group_child_validator(ip_group_child):
raise serializers.ValidationError(error)
def address_validator(value):
parsed = urlparse(value)
is_basic_url = parsed.scheme in ('http', 'https') and parsed.netloc
is_valid = value == '*' \
or is_ip_address(value) \
or is_ip_network(value) \
or is_ip_segment(value) \
or is_basic_url
if not is_valid:
error = _('address invalid: `{}`').format(value)
raise serializers.ValidationError(error)
ip_group_help_text = _(
'With * indicating a match all. '
'Such as: '

View File

@@ -1,3 +1,5 @@
from collections import Counter
__all__ = ['FormatAssetInfo']
@@ -7,13 +9,28 @@ class FormatAssetInfo:
self.tp = tp
@staticmethod
def posix_format(info):
for cpu_model in info.get('cpu_model', []):
if cpu_model.endswith('GHz') or cpu_model.startswith("Intel"):
break
else:
cpu_model = ''
info['cpu_model'] = cpu_model[:48]
def get_cpu_model_count(cpus):
try:
if len(cpus) % 3 == 0:
step = 3
models = [cpus[i + 2] for i in range(0, len(cpus), step)]
elif len(cpus) % 2 == 0:
step = 2
models = [cpus[i + 1] for i in range(0, len(cpus), step)]
else:
raise ValueError("CPU list format not recognized")
model_counts = Counter(models)
result = ', '.join([f"{model} x{count}" for model, count in model_counts.items()])
except Exception as e:
print(f"Error processing CPU model list: {e}")
result = ''
return result
def posix_format(self, info):
cpus = self.get_cpu_model_count(info.get('cpu_model', []))
info['cpu_model'] = cpus
info['cpu_count'] = info.get('cpu_count', 0)
return info

View File

@@ -16,3 +16,5 @@
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
connection_options:
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
register: result
failed_when: not result.is_available

View File

@@ -37,6 +37,7 @@ class DatabaseTypes(BaseType):
'verify_account_enabled': True,
'change_secret_enabled': True,
'push_account_enabled': True,
'remove_account_enabled': True,
},
cls.REDIS: {
'ansible_enabled': False,

View File

@@ -53,7 +53,8 @@ class HostTypes(BaseType):
'gather_accounts_enabled': True,
'verify_account_enabled': True,
'change_secret_enabled': True,
'push_account_enabled': True
'push_account_enabled': True,
'remove_account_enabled': True,
},
cls.WINDOWS: {
'ansible_config': {

View File

@@ -27,6 +27,7 @@ class PlatformAutomationSerializer(serializers.ModelSerializer):
"change_secret_enabled", "change_secret_method", "change_secret_params",
"verify_account_enabled", "verify_account_method", "verify_account_params",
"gather_accounts_enabled", "gather_accounts_method", "gather_accounts_params",
"remove_account_enabled", "remove_account_method", "remove_account_params",
]
extra_kwargs = {
# 启用资产探测
@@ -42,6 +43,8 @@ class PlatformAutomationSerializer(serializers.ModelSerializer):
"push_account_method": {"label": _("Push account method")},
"gather_accounts_enabled": {"label": _("Gather accounts enabled")},
"gather_accounts_method": {"label": _("Gather accounts method")},
"remove_account_method": {"label": _("Remove account method")},
"remove_account_enabled": {"label": _("Remove account enabled")},
}

View File

@@ -472,6 +472,8 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
rbac_perms = {
'create': 'authentication.add_superconnectiontoken',
'renewal': 'authentication.add_superconnectiontoken',
'list': 'authentication.view_superconnectiontoken',
'retrieve': 'authentication.view_superconnectiontoken',
'get_secret_detail': 'authentication.view_superconnectiontokensecret',
'get_applet_info': 'authentication.view_superconnectiontoken',
'release_applet_account': 'authentication.view_superconnectiontoken',
@@ -479,7 +481,12 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
}
def get_queryset(self):
return ConnectionToken.objects.all()
return ConnectionToken.objects.none()
def get_object(self):
pk = self.kwargs.get(self.lookup_field)
token = get_object_or_404(ConnectionToken, pk=pk)
return token
def get_user(self, serializer):
return serializer.validated_data.get('user')

View File

@@ -128,7 +128,7 @@ class SignatureAuthentication(signature.SignatureAuthentication):
# example implementation:
try:
key = AccessKey.objects.get(id=key_id)
if not key.is_active:
if not key.is_valid:
return None, None
user, secret = key.user, str(key.secret)
after_authenticate_update_date(user, key)

View File

@@ -36,7 +36,7 @@ class MFAMiddleware:
# 这个是 mfa 登录页需要的请求, 也得放出来, 用户其实已经在 CAS/OIDC 中完成登录了
white_urls = [
'login/mfa', 'mfa/select', 'jsi18n/', '/static/',
'/profile/otp', '/logout/',
'/profile/otp', '/logout/', '/media/'
]
for url in white_urls:
if request.path.find(url) > -1:
@@ -77,6 +77,7 @@ class ThirdPartyLoginMiddleware(mixins.AuthMixin):
ip = get_request_ip(request)
try:
self.request = request
self.check_is_block()
self._check_third_party_login_acl()
self._check_login_acl(request.user, ip)
except Exception as e:

View File

@@ -26,6 +26,10 @@ class AccessKey(models.Model):
date_last_used = models.DateTimeField(null=True, blank=True, verbose_name=_('Date last used'))
date_created = models.DateTimeField(auto_now_add=True)
@property
def is_valid(self):
return self.is_active and self.user.is_valid
def get_id(self):
return str(self.id)

View File

@@ -3,13 +3,16 @@
import json
import os
import re
import time
from urllib.parse import urlparse, quote
import pytz
import time
from django.conf import settings
from django.core.exceptions import MiddlewareNotUsed
from django.http.response import HttpResponseForbidden
from django.shortcuts import HttpResponse
from django.shortcuts import redirect
from django.urls import reverse
from django.utils import timezone
from .utils import set_current_request
@@ -137,3 +140,38 @@ class EndMiddleware:
response = self.get_response(request)
request._e_time_end = time.time()
return response
class SafeRedirectMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
if not (300 <= response.status_code < 400):
return response
if request.resolver_match and request.resolver_match.namespace.startswith('authentication'):
# 认证相关的路由跳过验证core/auth/xxxx
return response
location = response.get('Location')
if not location:
return response
parsed = urlparse(location)
if parsed.scheme and parsed.netloc:
target_host = parsed.netloc
if target_host in [*settings.ALLOWED_HOSTS]:
return response
target_host, target_port = self._split_host_port(parsed.netloc)
origin_host, origin_port = self._split_host_port(request.get_host())
if target_host != origin_host:
safe_redirect_url = '%s?%s' % (reverse('redirect-confirm'), f'next={quote(location)}')
return redirect(safe_redirect_url)
return response
@staticmethod
def _split_host_port(netloc):
if ':' in netloc:
host, port = netloc.split(':', 1)
return host, port
return netloc, '80'

View File

@@ -181,6 +181,7 @@ MIDDLEWARE = [
'authentication.middleware.ThirdPartyLoginMiddleware',
'authentication.middleware.SessionCookieMiddleware',
'simple_history.middleware.HistoryRequestMiddleware',
'jumpserver.middleware.SafeRedirectMiddleware',
'jumpserver.middleware.EndMiddleware',
]

View File

@@ -39,6 +39,7 @@ app_view_patterns = [
path('common/', include('common.urls.view_urls'), name='common'),
re_path(r'flower/(?P<path>.*)', views.celery_flower_view, name='flower-view'),
path('download/', views.ResourceDownload.as_view(), name='download'),
path('redirect/confirm/', views.RedirectConfirm.as_view(), name='redirect-confirm'),
path('i18n/<str:lang>/', views.I18NView.as_view(), name='i18n-switch'),
]

View File

@@ -1,9 +1,11 @@
# -*- coding: utf-8 -*-
#
import re
from urllib.parse import urlparse
from django.conf import settings
from django.http import HttpResponse
from django.http import HttpResponseBadRequest
from django.http import HttpResponseRedirect, JsonResponse, Http404
from django.shortcuts import redirect
from django.utils.translation import gettext_lazy as _
@@ -16,7 +18,7 @@ from common.views.http import HttpResponseTemporaryRedirect
__all__ = [
'LunaView', 'I18NView', 'KokoView', 'WsView',
'redirect_format_api', 'redirect_old_apps_view', 'UIView',
'ResourceDownload',
'ResourceDownload', 'RedirectConfirm'
]
@@ -94,3 +96,24 @@ def csrf_failure(request, reason=""):
from django.shortcuts import reverse
login_url = reverse('authentication:login') + '?csrf_failure=1&admin=1'
return redirect(login_url)
class RedirectConfirm(TemplateView):
template_name = 'redirect_confirm.html'
def get(self, request, *args, **kwargs):
next_url = self.request.GET.get("next")
if not self.is_valid_url(next_url):
return HttpResponseBadRequest("Invalid next url")
return self.render_to_response({"target_url": next_url})
@staticmethod
def is_valid_url(url):
if not url:
return False
parsed = urlparse(url)
if not parsed.scheme or not parsed.netloc:
return False
if parsed.scheme not in ['http', 'https']:
return False
return True

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2dd9ffcfe15b130a5b3d7b4fcfe806eaae979973e8bd29ad9a473b9215424c57
size 178725
oid sha256:ab3807ddd5c7b4c2dae9ebe80a6cf20c8ec4e189ea95a1581f8acc3d377e52f2
size 178402

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4517c6a7464c68f949912b97c8a9abcc766ca19e32267a1d1da3f0e012471c1a
size 146255
oid sha256:9d5e378a6e129625a1cc2b7b043e8feb3a48b0c239239d0ab954525b51244969
size 146503

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7d4d2709e597e055072474f08be2f363d43df239240051024a77213ba48ecfac
size 146366
oid sha256:884dde4efaa8a8a80efee337248c6a7c1ff5fc83e486e34db685a1d20479e109
size 146115

File diff suppressed because it is too large Load Diff

30
apps/orgs/mixins/ws.py Normal file
View File

@@ -0,0 +1,30 @@
from http.cookies import SimpleCookie
from asgiref.sync import sync_to_async
from orgs.utils import tmp_to_org
class OrgMixin:
cookie = None
org = None
def get_cookie(self):
try:
headers = self.scope['headers']
headers_dict = {key.decode('utf-8'): value.decode('utf-8') for key, value in headers}
cookie = SimpleCookie(headers_dict.get('cookie', ''))
except Exception as e:
cookie = SimpleCookie()
return cookie
def get_current_org(self):
oid = self.cookie.get('X-JMS-ORG')
return oid.value if oid else None
@sync_to_async
def has_perms(self, user, perms):
self.cookie = self.get_cookie()
self.org = self.get_current_org()
with tmp_to_org(self.org):
return user.has_perms(perms)

View File

@@ -185,6 +185,11 @@ class UserPermedNodeChildrenWithAssetsAsCategoryTreeApi(BaseUserNodeWithAssetAsT
return [], []
if not self.tp or not all(self.tp):
nodes = UserPermAssetUtil.get_type_nodes_tree_or_cached(self.user)
if self.request.query_params.get('count_resource'):
# 解决在 lina 使用该 api 类型树套娃问题
for node in nodes:
if node.get('meta'):
node['isParent'] = False
return nodes, []
category, tp = self.tp

View File

@@ -0,0 +1,19 @@
# Generated by Django 4.1.13 on 2025-08-25 03:03
import common.utils.django
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('perms', '0036_auto_20231108_1626'),
]
operations = [
migrations.AlterField(
model_name='assetpermission',
name='date_expired',
field=models.DateTimeField(db_index=True, default=common.utils.django.asset_permission_date_expired_default, verbose_name='Date expired'),
),
]

View File

@@ -49,6 +49,7 @@ def check_asset_permission_will_expired():
org_perm_remain_day_mapper = defaultdict(dict)
asset_perms = AssetPermission.objects.filter(
is_active=True,
date_expired__gte=start,
date_expired__lte=end
).distinct()

View File

@@ -1,21 +1,22 @@
# -*- coding: utf-8 -*-
#
import json
import asyncio
import json
from channels.generic.websocket import AsyncJsonWebsocketConsumer
from django.core.cache import cache
from django.conf import settings
from django.core.cache import cache
from django.utils.translation import gettext_lazy as _
from common.db.utils import close_old_connections
from common.utils import get_logger
from orgs.mixins.ws import OrgMixin
from orgs.models import Organization
from orgs.utils import current_org
from settings.serializers import (
LDAPTestConfigSerializer,
LDAPTestLoginSerializer
)
from orgs.models import Organization
from orgs.utils import current_org
from settings.tasks import sync_ldap_user
from settings.utils import (
LDAPServerUtil, LDAPCacheUtil, LDAPImportUtil, LDAPSyncUtil,
@@ -97,10 +98,10 @@ class ToolsWebsocket(AsyncJsonWebsocketConsumer):
close_old_connections()
class LdapWebsocket(AsyncJsonWebsocketConsumer):
class LdapWebsocket(AsyncJsonWebsocketConsumer, OrgMixin):
async def connect(self):
user = self.scope["user"]
if user.is_authenticated:
if user.is_authenticated and await self.has_perms(user, ['settings.view_setting']):
await self.accept()
else:
await self.close()
@@ -133,7 +134,7 @@ class LdapWebsocket(AsyncJsonWebsocketConsumer):
attr_map = serializer.validated_data["AUTH_LDAP_USER_ATTR_MAP"]
auth_ldap = serializer.validated_data.get('AUTH_LDAP', False)
if not password:
if not password and server_uri == settings.AUTH_LDAP_SERVER_URI:
password = settings.AUTH_LDAP_BIND_PASSWORD
config = {

View File

@@ -0,0 +1,44 @@
{% extends '_base_only_content.html' %}
{% load static %}
{% load i18n %}
{% block html_title %} {{ INTERFACE.login_title }} {% endblock %}
{% block title %} {{ INTERFACE.login_title }}{% endblock %}
{% block content %}
<style>
.alert.alert-msg {
background: #F5F5F7;
}
.target-url {
display: inline-block;
max-width: 100%;
min-width: 100px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
}
</style>
<div>
<p>
<div class="alert {% if error %} alert-danger {% else %} alert-info {% endif %}" id="messages">
{% trans 'You are about to be redirected to an external website. Please confirm that you trust this link: ' %}
<a class="target-url" href="{{ target_url }}">{{ target_url }}</a>
</div>
</p>
<div class="row">
<div class="col-sm-3">
<a href="/" class="btn btn-default block full-width m-b">
{% trans 'Cancel' %}
</a>
</div>
<div class="col-sm-3">
<a href="{{ target_url }}" class="btn btn-primary block full-width m-b">
{% trans 'Confirm' %}
</a>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,7 +1,7 @@
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from acls.serializers.rules import ip_group_child_validator, ip_group_help_text
from acls.serializers.rules import address_validator, ip_group_help_text
from common.serializers import BulkModelSerializer
from common.serializers.fields import ObjectRelatedField
from ..models import Endpoint, EndpointRule
@@ -61,13 +61,13 @@ class EndpointSerializer(BulkModelSerializer):
class EndpointRuleSerializer(BulkModelSerializer):
_ip_group_help_text = '{}, {} <br>{}'.format(
_('The assets within this IP range, the following endpoint will be used for the connection'),
_('The assets within this IP range or Host, the following endpoint will be used for the connection'),
_('If asset IP addresses under different endpoints conflict, use asset labels'),
ip_group_help_text,
)
ip_group = serializers.ListField(
default=['*'], label=_('Asset IP'), help_text=_ip_group_help_text,
child=serializers.CharField(max_length=1024, validators=[ip_group_child_validator])
default=['*'], label=_('Address'), help_text=_ip_group_help_text,
child=serializers.CharField(max_length=1024, validators=[address_validator]),
)
endpoint = ObjectRelatedField(
allow_null=True, required=False, queryset=Endpoint.objects, label=_('Endpoint')

View File

@@ -23,7 +23,8 @@ class TicketFilter(BaseFilterSet):
def filter_assignees_id(self, queryset, name, value):
return queryset.filter(
ticket_steps__ticket_assignees__assignee__id=value
ticket_steps__level=F('approval_step'),
ticket_steps__ticket_assignees__assignee_id=value
)
def filter_relevant_asset(self, queryset, name, value):

View File

@@ -0,0 +1,19 @@
# Generated by Django 4.1.13 on 2025-08-25 03:03
import common.utils.django
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0050_user_lark_id_alter_user_source'),
]
operations = [
migrations.AlterField(
model_name='user',
name='date_expired',
field=models.DateTimeField(blank=True, db_index=True, default=common.utils.django.user_date_expired_default, null=True, verbose_name='Date expired'),
),
]

View File

@@ -125,7 +125,7 @@ class BlockUtilBase:
BLOCK_KEY_TMPL: str
def __init__(self, username, ip):
self.username = username
username = username.lower() if username else ''
self.ip = ip
self.limit_key = self.LIMIT_KEY_TMPL.format(username, ip)
self.block_key = self.BLOCK_KEY_TMPL.format(username)

518
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -147,7 +147,7 @@ mistune = "2.0.3"
openai = "^1.29.0"
xlsxwriter = "^3.1.9"
exchangelib = "^5.1.0"
xmlsec = "^1.3.13"
xmlsec = "1.3.14"
lxml = "5.2.1"
receptorctl = "^1.4.5"
pydantic = "^2.7.4"