feat: add configurable expiration notice settings

This commit is contained in:
Crane.z
2026-07-28 16:56:12 +08:00
parent 57cc34895d
commit 1bfe0c4002
10 changed files with 99 additions and 11 deletions

View File

@@ -16,6 +16,20 @@ def local_now():
return dj_timezone.localtime(dj_timezone.now())
def expiration_remain_days(date_expired, today=None):
if today is None:
today = local_now().date()
date_expired = dj_timezone.localtime(date_expired).date()
return (date_expired - today).days
def should_notify_expiration(remain_days, first_notice_days, daily_notice_days):
return (
remain_days == first_notice_days
or 0 <= remain_days <= daily_notice_days
)
def local_now_display(fmt='%Y-%m-%d %H:%M:%S'):
return local_now().strftime(fmt)

View File

@@ -1860,5 +1860,13 @@
"LabelFilterAllValues": "全部值",
"LabelFilterSelectKey": "请选择标签键",
"LabelFilterKeyLabel": "键",
"LabelFilterValueLabel": "值"
"LabelFilterValueLabel": "值",
"NotificationSettings": "通知设置",
"FirstNotice": "首次提醒",
"DailyNotice": "每日提醒",
"NoticeAdvance": "提前",
"NoticeDay": "天",
"NoticeStart": "开始",
"PositiveIntegerRequired": "请输入正整数。",
"FirstNoticeNotLessThanDailyNotice": "首次提醒天数必须大于或等于每日提醒天数。"
}

View File

@@ -676,6 +676,10 @@ class Config(dict):
'PERM_EXPIRED_CHECK_PERIODIC': 60 * 60,
'PERM_EXPIRED_NOTICE_DAYS': 3,
'USER_EXPIRED_FIRST_NOTICE_DAYS': 5,
'USER_EXPIRED_DAILY_NOTICE_DAYS': 5,
'PERM_EXPIRED_FIRST_NOTICE_DAYS': 3,
'PERM_EXPIRED_DAILY_NOTICE_DAYS': 3,
'PERM_TREE_REGEN_INTERVAL': 1,
'FLOWER_URL': "127.0.0.1:5555",
'LANGUAGE_CODE': 'en',
@@ -812,6 +816,8 @@ class Config(dict):
old_config_map = {
'CONNECTION_TOKEN_ONETIME_EXPIRATION': 'CONNECTION_TOKEN_EXPIRATION',
'CONNECTION_TOKEN_REUSABLE_EXPIRATION': 'CONNECTION_TOKEN_EXPIRATION_MAX',
'PERM_EXPIRED_FIRST_NOTICE_DAYS': 'PERM_EXPIRED_NOTICE_DAYS',
'PERM_EXPIRED_DAILY_NOTICE_DAYS': 'PERM_EXPIRED_NOTICE_DAYS',
}
def __init__(self, *args):

View File

@@ -111,6 +111,10 @@ TICKET_AUTHORIZE_DEFAULT_TIME = CONFIG.TICKET_AUTHORIZE_DEFAULT_TIME
TICKET_AUTHORIZE_DEFAULT_TIME_UNIT = CONFIG.TICKET_AUTHORIZE_DEFAULT_TIME_UNIT
PERM_EXPIRED_CHECK_PERIODIC = CONFIG.PERM_EXPIRED_CHECK_PERIODIC
PERM_EXPIRED_NOTICE_DAYS = CONFIG.PERM_EXPIRED_NOTICE_DAYS
USER_EXPIRED_FIRST_NOTICE_DAYS = CONFIG.USER_EXPIRED_FIRST_NOTICE_DAYS
USER_EXPIRED_DAILY_NOTICE_DAYS = CONFIG.USER_EXPIRED_DAILY_NOTICE_DAYS
PERM_EXPIRED_FIRST_NOTICE_DAYS = CONFIG.PERM_EXPIRED_FIRST_NOTICE_DAYS
PERM_EXPIRED_DAILY_NOTICE_DAYS = CONFIG.PERM_EXPIRED_DAILY_NOTICE_DAYS
FLOWER_URL = CONFIG.FLOWER_URL
# Enable internal period task

View File

@@ -11,7 +11,9 @@ from django.utils.translation import gettext_lazy as _
from common.const.crontab import CRONTAB_AT_AM_TEN
from common.utils import get_logger
from common.utils.timezone import local_now, dt_parser
from common.utils.timezone import (
dt_parser, expiration_remain_days, local_now, should_notify_expiration,
)
from ops.celery.decorator import register_as_period_task
from orgs.utils import tmp_to_root_org
from perms.models import AssetPermission
@@ -58,8 +60,10 @@ def check_asset_permission_expired():
@atomic()
@tmp_to_root_org()
def check_asset_permission_will_expired():
start = local_now()
end = start + timedelta(days=settings.PERM_EXPIRED_NOTICE_DAYS)
first_notice_days = settings.PERM_EXPIRED_FIRST_NOTICE_DAYS
daily_notice_days = settings.PERM_EXPIRED_DAILY_NOTICE_DAYS
start = local_now().replace(hour=0, minute=0, second=0, microsecond=0)
end = start + timedelta(days=max(first_notice_days, daily_notice_days) + 1)
user_asset_remain_day_mapper = defaultdict(dict)
org_perm_remain_day_mapper = defaultdict(dict)
@@ -72,7 +76,10 @@ def check_asset_permission_will_expired():
for asset_perm in asset_perms:
date_expired = dt_parser(asset_perm.date_expired)
remain_days = (date_expired - start).days
remain_days = expiration_remain_days(date_expired, start.date())
if not should_notify_expiration(
remain_days, first_notice_days, daily_notice_days):
continue
org = asset_perm.org
# 资产授权按照组织分类

View File

@@ -73,6 +73,7 @@ class SettingsApi(generics.RetrieveUpdateAPIView):
'ops': serializers.OpsSettingSerializer,
'virtualapp': serializers.VirtualAppSerializer,
'tool': serializers.ToolSerializer,
'task_notice': serializers.TaskNoticeSettingSerializer,
}
rbac_category_permissions = {
@@ -89,6 +90,7 @@ class SettingsApi(generics.RetrieveUpdateAPIView):
'security_session': 'settings.change_security',
'security_password': 'settings.change_security',
'security_login_limit': 'settings.change_security',
'task_notice': 'settings.change_security',
'ldap': 'settings.change_auth',
'ldap_ha': 'settings.change_auth',
'cas': 'settings.change_auth',

View File

@@ -12,5 +12,6 @@ from .prompt import *
from .public import *
from .security import *
from .settings import *
from .task import *
from .terminal import *
from .tool import *

View File

@@ -17,6 +17,7 @@ from .cleaning import CleaningSerializer
from .msg import EmailSettingSerializer, EmailContentSettingSerializer
from .other import OtherSettingSerializer
from .security import SecuritySettingSerializer
from .task import TaskNoticeSettingSerializer
from .terminal import TerminalSettingSerializer
__all__ = [
@@ -90,7 +91,8 @@ class SettingsSerializer(
TencentSMSSettingSerializer,
CMPP2SMSSettingSerializer,
CustomSMSSettingSerializer,
PasskeySettingSerializer
PasskeySettingSerializer,
TaskNoticeSettingSerializer,
):
PREFIX_TITLE = _('Setting')
CACHE_KEY = 'SETTING_FIELDS_MAPPING'

View File

@@ -0,0 +1,36 @@
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
__all__ = ['TaskNoticeSettingSerializer']
class TaskNoticeSettingSerializer(serializers.Serializer):
USER_EXPIRED_FIRST_NOTICE_DAYS = serializers.IntegerField(
min_value=1, max_value=3650, label=_('First notice'),
)
USER_EXPIRED_DAILY_NOTICE_DAYS = serializers.IntegerField(
min_value=1, max_value=365, label=_('Daily notice'),
)
PERM_EXPIRED_FIRST_NOTICE_DAYS = serializers.IntegerField(
min_value=1, max_value=3650, label=_('First notice'),
)
PERM_EXPIRED_DAILY_NOTICE_DAYS = serializers.IntegerField(
min_value=1, max_value=365, label=_('Daily notice'),
)
notice_field_pairs = (
('USER_EXPIRED_FIRST_NOTICE_DAYS', 'USER_EXPIRED_DAILY_NOTICE_DAYS'),
('PERM_EXPIRED_FIRST_NOTICE_DAYS', 'PERM_EXPIRED_DAILY_NOTICE_DAYS'),
)
def validate(self, attrs):
for first_name, daily_name in self.notice_field_pairs:
if first_name not in attrs and daily_name not in attrs:
continue
first_days = attrs.get(first_name, getattr(settings, first_name))
daily_days = attrs.get(daily_name, getattr(settings, daily_name))
if first_days < daily_days:
message = _('First notice days must be greater than or equal to daily notice days.')
raise serializers.ValidationError({first_name: message})
return attrs

View File

@@ -12,6 +12,9 @@ from django.utils.translation import gettext_lazy as _, gettext_noop
from audits.const import ActivityChoices
from common.const.crontab import CRONTAB_AT_AM_TEN, CRONTAB_AT_PM_TWO
from common.utils import get_logger
from common.utils.timezone import (
expiration_remain_days, local_now, should_notify_expiration,
)
from ops.celery.decorator import after_app_ready_start, register_as_period_task
from ops.celery.utils import create_or_update_celery_periodic_tasks
from orgs.utils import tmp_to_root_org
@@ -71,18 +74,23 @@ def check_password_expired_periodic():
)
)
def check_user_expired():
date_expired_lt = timezone.now() + timezone.timedelta(days=User.DATE_EXPIRED_WARNING_DAYS)
first_notice_days = settings.USER_EXPIRED_FIRST_NOTICE_DAYS
daily_notice_days = settings.USER_EXPIRED_DAILY_NOTICE_DAYS
start = local_now().replace(hour=0, minute=0, second=0, microsecond=0)
end = start + timedelta(days=max(first_notice_days, daily_notice_days) + 1)
users = User.get_nature_users() \
.filter(source=User.Source.local) \
.filter(date_expired__lt=date_expired_lt)
.filter(date_expired__gte=start, date_expired__lt=end)
for user in users:
if not user.is_valid:
if not user.is_active:
continue
if not user.will_expired:
remain_days = expiration_remain_days(user.date_expired, start.date())
if not should_notify_expiration(
remain_days, first_notice_days, daily_notice_days):
continue
msg = "The user {} will expires in {} days"
logger.info(msg.format(user, user.expired_remain_days))
logger.info(msg.format(user, remain_days))
UserExpirationReminderMsg(user).publish_async()