2023-11-07 05:00:09 +00:00
|
|
|
|
from collections import defaultdict
|
|
|
|
|
|
2024-11-11 03:12:10 +00:00
|
|
|
|
from django.utils import timezone
|
|
|
|
|
|
2023-02-21 12:06:45 +00:00
|
|
|
|
from accounts.const import AutomationTypes
|
2024-11-11 03:12:10 +00:00
|
|
|
|
from accounts.models import GatheredAccount, Account, AccountRisk
|
2023-11-07 05:00:09 +00:00
|
|
|
|
from assets.models import Asset
|
2024-10-28 10:57:57 +00:00
|
|
|
|
from common.const import ConfirmOrIgnore
|
2024-11-19 10:05:59 +00:00
|
|
|
|
from common.decorators import bulk_create_decorator, bulk_update_decorator
|
2023-02-21 12:06:45 +00:00
|
|
|
|
from common.utils import get_logger
|
2024-10-31 09:03:23 +00:00
|
|
|
|
from common.utils.strings import get_text_diff
|
2023-02-21 12:06:45 +00:00
|
|
|
|
from orgs.utils import tmp_to_org
|
2022-10-27 10:53:10 +00:00
|
|
|
|
from .filter import GatherAccountsFilter
|
2023-01-16 11:02:09 +00:00
|
|
|
|
from ..base.manager import AccountBasePlaybookManager
|
2023-11-07 05:00:09 +00:00
|
|
|
|
from ...notifications import GatherAccountChangeMsg
|
2022-10-27 10:53:10 +00:00
|
|
|
|
|
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
|
|
|
|
|
2024-11-18 11:06:04 +00:00
|
|
|
|
diff_items = [
|
|
|
|
|
'authorized_keys', 'sudoers', 'groups',
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_items_diff(ori_account, d):
|
|
|
|
|
if hasattr(ori_account, '_diff'):
|
|
|
|
|
return ori_account._diff
|
|
|
|
|
|
|
|
|
|
diff = {}
|
|
|
|
|
for item in diff_items:
|
|
|
|
|
ori = getattr(ori_account, item)
|
|
|
|
|
new = d.get(item, '')
|
|
|
|
|
|
|
|
|
|
if not ori:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
if isinstance(new, timezone.datetime):
|
|
|
|
|
new = ori.strftime('%Y-%m-%d %H:%M:%S')
|
|
|
|
|
ori = ori.strftime('%Y-%m-%d %H:%M:%S')
|
|
|
|
|
if new != ori:
|
|
|
|
|
diff[item] = get_text_diff(ori, new)
|
|
|
|
|
|
|
|
|
|
ori_account._diff = diff
|
|
|
|
|
return diff
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AnalyseAccountRisk:
|
2024-11-11 03:12:10 +00:00
|
|
|
|
long_time = timezone.timedelta(days=90)
|
2024-11-12 08:00:41 +00:00
|
|
|
|
datetime_check_items = [
|
|
|
|
|
{'field': 'date_last_login', 'risk': 'zombie', 'delta': long_time},
|
|
|
|
|
{'field': 'date_password_change', 'risk': 'long_time_password', 'delta': long_time},
|
|
|
|
|
{'field': 'date_password_expired', 'risk': 'password_expired', 'delta': timezone.timedelta(seconds=1)}
|
|
|
|
|
]
|
2024-10-31 09:03:23 +00:00
|
|
|
|
|
2024-11-18 11:06:04 +00:00
|
|
|
|
def __init__(self, check_risk=True):
|
|
|
|
|
self.check_risk = check_risk
|
|
|
|
|
self.now = timezone.now()
|
|
|
|
|
self.pending_add_risks = []
|
|
|
|
|
|
|
|
|
|
def _analyse_item_changed(self, ori_account, d):
|
|
|
|
|
diff = get_items_diff(ori_account, d)
|
|
|
|
|
|
|
|
|
|
if not diff:
|
|
|
|
|
return
|
|
|
|
|
|
2024-11-19 10:05:59 +00:00
|
|
|
|
risks = []
|
2024-11-18 11:06:04 +00:00
|
|
|
|
for k, v in diff.items():
|
2024-11-19 10:05:59 +00:00
|
|
|
|
risks.append(dict(
|
2024-11-18 11:06:04 +00:00
|
|
|
|
asset=ori_account.asset, username=ori_account.username,
|
|
|
|
|
risk=k+'_changed', detail={'diff': v}
|
|
|
|
|
))
|
2024-11-19 10:05:59 +00:00
|
|
|
|
self.save_or_update_risks(risks)
|
2024-11-18 11:06:04 +00:00
|
|
|
|
|
|
|
|
|
def _analyse_datetime_changed(self, ori_account, d, asset, username):
|
|
|
|
|
basic = {'asset': asset, 'username': username}
|
|
|
|
|
|
2024-11-19 10:05:59 +00:00
|
|
|
|
risks = []
|
2024-11-18 11:06:04 +00:00
|
|
|
|
for item in self.datetime_check_items:
|
|
|
|
|
field = item['field']
|
|
|
|
|
risk = item['risk']
|
|
|
|
|
delta = item['delta']
|
|
|
|
|
|
|
|
|
|
date = d.get(field)
|
|
|
|
|
if not date:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
pre_date = ori_account and getattr(ori_account, field)
|
|
|
|
|
if pre_date == date:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
if date and date < timezone.now() - delta:
|
2024-11-19 10:05:59 +00:00
|
|
|
|
risks.append(
|
2024-11-18 11:06:04 +00:00
|
|
|
|
dict(**basic, risk=risk, detail={'date': date.isoformat()})
|
|
|
|
|
)
|
|
|
|
|
|
2024-11-19 10:05:59 +00:00
|
|
|
|
self.save_or_update_risks(risks)
|
|
|
|
|
|
|
|
|
|
def save_or_update_risks(self, risks):
|
|
|
|
|
# 提前取出来,避免每次都查数据库
|
|
|
|
|
assets = {r['asset'] for r in risks}
|
|
|
|
|
assets_risks = AccountRisk.objects.filter(asset__in=assets)
|
|
|
|
|
assets_risks = {f"{r.asset_id}_{r.username}_{r.risk}": r for r in assets_risks}
|
2024-11-18 11:06:04 +00:00
|
|
|
|
|
2024-11-19 10:05:59 +00:00
|
|
|
|
for d in risks:
|
|
|
|
|
detail = d.pop('detail', {})
|
|
|
|
|
detail['datetime'] = self.now.isoformat()
|
|
|
|
|
key = f"{d['asset'].id}_{d['username']}_{d['risk']}"
|
|
|
|
|
found = assets_risks.get(key)
|
|
|
|
|
|
|
|
|
|
if not found:
|
|
|
|
|
self._create_risk(dict(**d, details=[detail]))
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
found.details.append(detail)
|
|
|
|
|
self._update_risk(found)
|
|
|
|
|
|
|
|
|
|
@bulk_create_decorator(AccountRisk)
|
|
|
|
|
def _create_risk(self, data):
|
|
|
|
|
return AccountRisk(**data)
|
|
|
|
|
|
|
|
|
|
@bulk_update_decorator(AccountRisk, update_fields=['details'])
|
|
|
|
|
def _update_risk(self, account):
|
|
|
|
|
return account
|
|
|
|
|
|
|
|
|
|
def finish(self):
|
|
|
|
|
self._create_risk.finish()
|
|
|
|
|
self._update_risk.finish()
|
|
|
|
|
|
|
|
|
|
def analyse_risk(self, asset, ori_account, d):
|
|
|
|
|
if not self.check_risk:
|
2024-11-18 11:06:04 +00:00
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
basic = {'asset': asset, 'username': d['username']}
|
|
|
|
|
if ori_account:
|
|
|
|
|
self._analyse_item_changed(ori_account, d)
|
|
|
|
|
else:
|
2024-11-19 10:05:59 +00:00
|
|
|
|
self._create_risk(dict(**basic, risk='new_account'))
|
2024-11-18 11:06:04 +00:00
|
|
|
|
|
|
|
|
|
self._analyse_datetime_changed(ori_account, d, asset, d['username'])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class GatherAccountsManager(AccountBasePlaybookManager):
|
2022-10-27 10:53:10 +00:00
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
|
self.host_asset_mapper = {}
|
2023-11-15 08:44:35 +00:00
|
|
|
|
self.asset_account_info = {}
|
2024-10-31 09:03:23 +00:00
|
|
|
|
self.asset_usernames_mapper = defaultdict(set)
|
|
|
|
|
self.ori_asset_usernames = defaultdict(set)
|
|
|
|
|
self.ori_gathered_usernames = defaultdict(set)
|
|
|
|
|
self.ori_gathered_accounts_mapper = dict()
|
2023-03-23 10:57:22 +00:00
|
|
|
|
self.is_sync_account = self.execution.snapshot.get('is_sync_account')
|
2024-11-18 11:06:04 +00:00
|
|
|
|
self.check_risk = self.execution.snapshot.get('check_risk', False)
|
2022-10-27 10:53:10 +00:00
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def method_type(cls):
|
|
|
|
|
return AutomationTypes.gather_accounts
|
|
|
|
|
|
|
|
|
|
def host_callback(self, host, asset=None, **kwargs):
|
|
|
|
|
super().host_callback(host, asset=asset, **kwargs)
|
|
|
|
|
self.host_asset_mapper[host['name']] = asset
|
|
|
|
|
return host
|
|
|
|
|
|
2024-10-31 09:03:23 +00:00
|
|
|
|
def _filter_success_result(self, tp, result):
|
2023-02-28 10:45:38 +00:00
|
|
|
|
result = GatherAccountsFilter(tp).run(self.method_id_meta_mapper, result)
|
2022-10-27 10:53:10 +00:00
|
|
|
|
return result
|
2023-11-07 05:00:09 +00:00
|
|
|
|
|
2024-05-13 10:00:17 +00:00
|
|
|
|
@staticmethod
|
2024-10-31 09:03:23 +00:00
|
|
|
|
def _get_nested_info(data, *keys):
|
2024-05-13 10:00:17 +00:00
|
|
|
|
for key in keys:
|
|
|
|
|
data = data.get(key, {})
|
|
|
|
|
if not data:
|
|
|
|
|
break
|
|
|
|
|
return data
|
|
|
|
|
|
2024-10-31 09:03:23 +00:00
|
|
|
|
def _collect_asset_account_info(self, asset, info):
|
|
|
|
|
result = self._filter_success_result(asset.type, info)
|
|
|
|
|
accounts = []
|
|
|
|
|
for username, info in result.items():
|
|
|
|
|
self.asset_usernames_mapper[asset].add(username)
|
|
|
|
|
|
2024-11-01 10:49:03 +00:00
|
|
|
|
d = {'asset': asset, 'username': username, 'remote_present': True, **info}
|
2024-10-31 09:03:23 +00:00
|
|
|
|
accounts.append(d)
|
|
|
|
|
self.asset_account_info[asset] = accounts
|
|
|
|
|
|
|
|
|
|
def on_runner_failed(self, runner, e):
|
2024-11-12 08:00:41 +00:00
|
|
|
|
print("Runner failed: ", e)
|
2024-10-31 09:03:23 +00:00
|
|
|
|
raise e
|
|
|
|
|
|
2022-10-27 10:53:10 +00:00
|
|
|
|
def on_host_success(self, host, result):
|
2024-10-31 09:03:23 +00:00
|
|
|
|
info = self._get_nested_info(result, 'debug', 'res', 'info')
|
2022-10-27 10:53:10 +00:00
|
|
|
|
asset = self.host_asset_mapper.get(host)
|
|
|
|
|
if asset and info:
|
2024-10-31 09:03:23 +00:00
|
|
|
|
self._collect_asset_account_info(asset, info)
|
2022-10-27 10:53:10 +00:00
|
|
|
|
else:
|
2024-03-21 03:05:04 +00:00
|
|
|
|
print(f'\033[31m Not found {host} info \033[0m\n')
|
2024-10-31 09:03:23 +00:00
|
|
|
|
|
|
|
|
|
def prefetch_origin_account_usernames(self):
|
|
|
|
|
"""
|
|
|
|
|
提起查出来,避免每次 sql 查询
|
|
|
|
|
:return:
|
|
|
|
|
"""
|
|
|
|
|
assets = self.asset_usernames_mapper.keys()
|
|
|
|
|
accounts = Account.objects.filter(asset__in=assets).values_list('asset', 'username')
|
2023-11-07 05:00:09 +00:00
|
|
|
|
|
2024-10-31 09:03:23 +00:00
|
|
|
|
for asset, username in accounts:
|
|
|
|
|
self.ori_asset_usernames[asset].add(username)
|
|
|
|
|
|
|
|
|
|
ga_accounts = GatheredAccount.objects.filter(asset__in=assets)
|
|
|
|
|
for account in ga_accounts:
|
|
|
|
|
self.ori_gathered_usernames[account.asset].add(account.username)
|
2024-11-19 10:05:59 +00:00
|
|
|
|
key = '{}_{}'.format(account.asset_id, account.username)
|
2024-10-31 09:03:23 +00:00
|
|
|
|
self.ori_gathered_accounts_mapper[key] = account
|
|
|
|
|
|
|
|
|
|
def update_gather_accounts_status(self, asset):
|
2024-10-30 08:10:46 +00:00
|
|
|
|
"""
|
2024-10-31 09:03:23 +00:00
|
|
|
|
远端账号,收集中的账号,vault 中的账号。
|
|
|
|
|
要根据账号新增见啥,标识 收集账号的状态, 让管理员关注
|
|
|
|
|
|
|
|
|
|
远端账号 -> 收集账号 -> 特权账号
|
2024-10-30 08:10:46 +00:00
|
|
|
|
"""
|
2024-10-31 09:03:23 +00:00
|
|
|
|
remote_users = self.asset_usernames_mapper[asset]
|
|
|
|
|
ori_users = self.ori_asset_usernames[asset]
|
|
|
|
|
ori_ga_users = self.ori_gathered_usernames[asset]
|
|
|
|
|
|
2024-11-01 08:40:36 +00:00
|
|
|
|
queryset = (GatheredAccount.objects
|
|
|
|
|
.filter(asset=asset)
|
|
|
|
|
.exclude(status=ConfirmOrIgnore.ignored))
|
|
|
|
|
|
2024-10-31 09:03:23 +00:00
|
|
|
|
# 远端账号 比 收集账号多的
|
|
|
|
|
# 新增创建,不用处理状态
|
2024-10-30 08:10:46 +00:00
|
|
|
|
|
2024-10-31 09:03:23 +00:00
|
|
|
|
# 远端上 比 收集账号少的
|
2024-11-01 10:49:03 +00:00
|
|
|
|
# 标识 remote_present=False, 标记为待处理
|
2024-10-30 08:10:46 +00:00
|
|
|
|
# 远端资产上不存在的,标识为待处理,需要管理员介入
|
2024-11-01 08:40:36 +00:00
|
|
|
|
lost_users = ori_ga_users - remote_users
|
2024-10-31 09:03:23 +00:00
|
|
|
|
if lost_users:
|
2024-11-01 10:49:03 +00:00
|
|
|
|
queryset.filter(username__in=lost_users).update(status='', remote_present=False)
|
2024-10-31 09:03:23 +00:00
|
|
|
|
|
|
|
|
|
# 收集的账号 比 账号列表多的, 有可能是账号中删掉了, 但这时候状态已经是 confirm 了
|
|
|
|
|
# 标识状态为 待处理, 让管理员去确认
|
|
|
|
|
ga_added_users = ori_ga_users - ori_users
|
|
|
|
|
if ga_added_users:
|
2024-11-01 08:40:36 +00:00
|
|
|
|
queryset.filter(username__in=ga_added_users).update(status='')
|
2024-10-31 09:03:23 +00:00
|
|
|
|
|
|
|
|
|
# 收集的账号 比 账号列表少的
|
|
|
|
|
# 这个好像不不用对比,原始情况就这样
|
|
|
|
|
|
|
|
|
|
# 远端账号 比 账号列表少的
|
2024-11-01 10:49:03 +00:00
|
|
|
|
# 创建收集账号,标识 remote_present=False, 状态待处理
|
2024-10-31 09:03:23 +00:00
|
|
|
|
|
|
|
|
|
# 远端账号 比 账号列表多的
|
|
|
|
|
# 正常情况, 不用处理,因为远端账号会创建到收集账号,收集账号再去对比
|
2024-11-01 08:40:36 +00:00
|
|
|
|
|
|
|
|
|
# 不过这个好像也处理一下 status,因为已存在,这是状态应该是确认
|
|
|
|
|
(queryset.filter(username__in=ori_users)
|
|
|
|
|
.exclude(status=ConfirmOrIgnore.confirmed)
|
|
|
|
|
.update(status=ConfirmOrIgnore.confirmed))
|
2024-11-01 10:49:03 +00:00
|
|
|
|
|
|
|
|
|
# 远端存在的账号,标识为已存在
|
|
|
|
|
queryset.filter(username__in=remote_users, remote_present=False).update(remote_present=True)
|
|
|
|
|
|
|
|
|
|
# 资产上没有的,标识为为存在
|
|
|
|
|
queryset.exclude(username__in=ori_users).filter(present=False).update(present=True)
|
2024-10-31 09:03:23 +00:00
|
|
|
|
|
2024-11-19 10:05:59 +00:00
|
|
|
|
@bulk_create_decorator(GatheredAccount)
|
|
|
|
|
def create_gathered_account(self, d):
|
2024-10-31 09:03:23 +00:00
|
|
|
|
gathered_account = GatheredAccount()
|
|
|
|
|
for k, v in d.items():
|
|
|
|
|
setattr(gathered_account, k, v)
|
2024-11-19 10:05:59 +00:00
|
|
|
|
return gathered_account
|
2024-11-11 03:12:10 +00:00
|
|
|
|
|
2024-11-19 10:05:59 +00:00
|
|
|
|
@bulk_update_decorator(GatheredAccount, update_fields=diff_items)
|
|
|
|
|
def update_gathered_account(self, ori_account, d):
|
2024-11-18 11:06:04 +00:00
|
|
|
|
diff = get_items_diff(ori_account, d)
|
2024-11-19 10:05:59 +00:00
|
|
|
|
if not diff:
|
|
|
|
|
return
|
|
|
|
|
for k in diff:
|
|
|
|
|
setattr(ori_account, k, d[k])
|
|
|
|
|
return ori_account
|
2024-10-30 08:10:46 +00:00
|
|
|
|
|
2024-11-19 10:05:59 +00:00
|
|
|
|
def do_run(self, *args, **kwargs):
|
|
|
|
|
super().do_run(*args, **kwargs)
|
|
|
|
|
self.prefetch_origin_account_usernames()
|
2024-11-18 11:06:04 +00:00
|
|
|
|
risk_analyser = AnalyseAccountRisk(self.check_risk)
|
|
|
|
|
|
2024-10-31 09:03:23 +00:00
|
|
|
|
for asset, accounts_data in self.asset_account_info.items():
|
2024-10-28 10:57:57 +00:00
|
|
|
|
with (tmp_to_org(asset.org_id)):
|
2023-11-15 08:44:35 +00:00
|
|
|
|
gathered_accounts = []
|
2024-10-31 09:03:23 +00:00
|
|
|
|
for d in accounts_data:
|
2023-11-15 08:44:35 +00:00
|
|
|
|
username = d['username']
|
2024-10-31 09:03:23 +00:00
|
|
|
|
ori_account = self.ori_gathered_accounts_mapper.get('{}_{}'.format(asset.id, username))
|
|
|
|
|
|
|
|
|
|
if not ori_account:
|
2024-11-19 10:05:59 +00:00
|
|
|
|
self.create_gathered_account(d)
|
2024-10-31 09:03:23 +00:00
|
|
|
|
else:
|
2024-11-19 10:05:59 +00:00
|
|
|
|
self.update_gathered_account(ori_account, d)
|
|
|
|
|
risk_analyser.analyse_risk(asset, ori_account, d)
|
2024-11-11 03:12:10 +00:00
|
|
|
|
|
2024-10-30 08:10:46 +00:00
|
|
|
|
self.update_gather_accounts_status(asset)
|
|
|
|
|
GatheredAccount.sync_accounts(gathered_accounts, self.is_sync_account)
|
2023-11-15 08:44:35 +00:00
|
|
|
|
|
2024-11-19 10:05:59 +00:00
|
|
|
|
self.create_gathered_account.finish()
|
|
|
|
|
self.update_gathered_account.finish()
|
|
|
|
|
risk_analyser.finish()
|
2024-10-31 09:03:23 +00:00
|
|
|
|
|
2024-11-19 10:05:59 +00:00
|
|
|
|
def send_report_if_need(self):
|
|
|
|
|
pass
|
2023-11-07 05:00:09 +00:00
|
|
|
|
|
2023-11-15 08:44:35 +00:00
|
|
|
|
def generate_send_users_and_change_info(self):
|
2023-11-07 05:00:09 +00:00
|
|
|
|
recipients = self.execution.recipients
|
2024-10-31 09:03:23 +00:00
|
|
|
|
if not self.asset_usernames_mapper or not recipients:
|
2023-11-15 08:44:35 +00:00
|
|
|
|
return None, None
|
2023-11-07 05:00:09 +00:00
|
|
|
|
|
2024-11-18 03:22:46 +00:00
|
|
|
|
users = recipients
|
2024-10-31 09:03:23 +00:00
|
|
|
|
asset_ids = self.asset_usernames_mapper.keys()
|
2024-08-27 08:46:41 +00:00
|
|
|
|
assets = Asset.objects.filter(id__in=asset_ids).prefetch_related('accounts')
|
2024-11-01 10:49:03 +00:00
|
|
|
|
gather_accounts = GatheredAccount.objects.filter(asset_id__in=asset_ids, remote_present=True)
|
2024-08-27 08:46:41 +00:00
|
|
|
|
|
2023-11-07 05:00:09 +00:00
|
|
|
|
asset_id_map = {str(asset.id): asset for asset in assets}
|
2023-11-15 08:44:35 +00:00
|
|
|
|
asset_id_username = list(assets.values_list('id', 'accounts__username'))
|
|
|
|
|
asset_id_username.extend(list(gather_accounts.values_list('asset_id', 'username')))
|
2023-11-07 05:00:09 +00:00
|
|
|
|
|
2024-10-31 09:03:23 +00:00
|
|
|
|
system_asset_usernames_mapper = defaultdict(set)
|
2023-11-15 08:44:35 +00:00
|
|
|
|
for asset_id, username in asset_id_username:
|
2024-10-31 09:03:23 +00:00
|
|
|
|
system_asset_usernames_mapper[str(asset_id)].add(username)
|
2023-11-07 05:00:09 +00:00
|
|
|
|
|
2024-08-27 08:46:41 +00:00
|
|
|
|
change_info = defaultdict(dict)
|
2024-10-31 09:03:23 +00:00
|
|
|
|
for asset_id, usernames in self.asset_usernames_mapper.items():
|
|
|
|
|
system_usernames = system_asset_usernames_mapper.get(asset_id)
|
2023-11-07 05:00:09 +00:00
|
|
|
|
if not system_usernames:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
add_usernames = usernames - system_usernames
|
|
|
|
|
remove_usernames = system_usernames - usernames
|
|
|
|
|
|
2023-11-14 06:50:33 +00:00
|
|
|
|
if not add_usernames and not remove_usernames:
|
|
|
|
|
continue
|
|
|
|
|
|
2024-08-27 08:46:41 +00:00
|
|
|
|
change_info[str(asset_id_map[asset_id])] = {
|
|
|
|
|
'add_usernames': add_usernames,
|
|
|
|
|
'remove_usernames': remove_usernames
|
2023-11-07 05:00:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
2024-08-27 08:46:41 +00:00
|
|
|
|
return users, dict(change_info)
|
2023-11-15 08:44:35 +00:00
|
|
|
|
|
2024-10-31 09:03:23 +00:00
|
|
|
|
def send_email_if_need(self):
|
|
|
|
|
users, change_info = self.generate_send_users_and_change_info()
|
2023-11-15 08:44:35 +00:00
|
|
|
|
if not users or not change_info:
|
2023-11-07 05:00:09 +00:00
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
for user in users:
|
|
|
|
|
GatherAccountChangeMsg(user, change_info).publish_async()
|