From 5d4fa2205811e7aaa5034c2955f1090262ec1348 Mon Sep 17 00:00:00 2001 From: maninhill <41712985+maninhill@users.noreply.github.com> Date: Fri, 22 Sep 2023 09:45:22 +0800 Subject: [PATCH 001/114] =?UTF-8?q?chore:=20=E4=BC=98=E5=8C=96=20README=20?= =?UTF-8?q?=E6=96=87=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index e83f8ee8f..c445740a2 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,6 @@
- JumpServer v3.0 正式发布。
-
9 年时间,倾情投入,用心做好一款开源堡垒机。
{% trans 'Username' %}: [{{ username }}]
+IP: [{{ ip }}]
+{% trans 'Login city' %}: [{{ city }}]
+{% trans 'User agent' %}: [{{ user_agent }}]
+{% trans 'The user has just successfully logged into the system. Please ensure that this is an authorized operation. If you suspect that this is an unauthorized access, please take appropriate measures immediately.' %}
+ +{% trans 'Thank you' %}!
+ diff --git a/apps/audits/signal_handlers/login_log.py b/apps/audits/signal_handlers/login_log.py index fae32a44b..835e4842f 100644 --- a/apps/audits/signal_handlers/login_log.py +++ b/apps/audits/signal_handlers/login_log.py @@ -11,6 +11,9 @@ from django.utils.functional import LazyObject from django.utils.translation import gettext_lazy as _ from rest_framework.request import Request +from acls.const import ActionChoices +from acls.models import LoginACL +from acls.notifications import UserLoginReminderMsg from audits.models import UserLoginLog from authentication.signals import post_auth_failed, post_auth_success from authentication.utils import check_different_city_login_if_need @@ -102,21 +105,37 @@ def create_user_session(request, user_id, instance: UserLoginLog): request.session['user_session_id'] = user_session.id +def send_login_info_to_reviewers(instance: UserLoginLog, reviewers): + for reviewer in reviewers: + UserLoginReminderMsg(reviewer, instance).publish_async() + + @receiver(post_auth_success) def on_user_auth_success(sender, user, request, login_type=None, **kwargs): logger.debug('User login success: {}'.format(user.username)) check_different_city_login_if_need(user, request) - data = generate_data( - user.username, request, login_type=login_type - ) - request.session['login_time'] = data['datetime'].strftime("%Y-%m-%d %H:%M:%S") + data = generate_data(user.username, request, login_type=login_type) + request.session['login_time'] = data['datetime'].strftime('%Y-%m-%d %H:%M:%S') data.update({'mfa': int(user.mfa_enabled), 'status': True}) instance = write_login_log(**data) + # TODO 目前只记录 web 登录的 session if instance.type != LoginTypeChoices.web: return create_user_session(request, user.id, instance) + auth_notice_required = request.session.get('auth_notice_required') + if not auth_notice_required: + return + + auth_acl_id = request.session.get('auth_acl_id') + acl = LoginACL.objects.filter(id=auth_acl_id, action=ActionChoices.notice).first() + if not acl or not acl.reviewers.exists(): + return + + reviewers = acl.reviewers.all() + send_login_info_to_reviewers(instance, reviewers) + @receiver(post_auth_failed) def on_user_auth_failed(sender, username, request, reason='', **kwargs): diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py index b301af6c3..9667e85c7 100644 --- a/apps/authentication/mixins.py +++ b/apps/authentication/mixins.py @@ -355,6 +355,11 @@ class AuthACLMixin: self.request.session['auth_acl_id'] = str(acl.id) return + if acl.is_action(acl.ActionChoices.notice): + self.request.session['auth_notice_required'] = '1' + self.request.session['auth_acl_id'] = str(acl.id) + return + def _check_third_party_login_acl(self): request = self.request error_message = getattr(request, 'error_message', None) @@ -513,7 +518,7 @@ class AuthMixin(CommonMixin, AuthPreCheckMixin, AuthACLMixin, MFAMixin, AuthPost def clear_auth_mark(self): keys = [ 'auth_password', 'user_id', 'auth_confirm_required', - 'auth_ticket_id', 'auth_acl_id' + 'auth_notice_required', 'auth_ticket_id', 'auth_acl_id' ] for k in keys: self.request.session.pop(k, '') diff --git a/apps/locale/ja/LC_MESSAGES/django.mo b/apps/locale/ja/LC_MESSAGES/django.mo index e12251412..8e58c4d50 100644 --- a/apps/locale/ja/LC_MESSAGES/django.mo +++ b/apps/locale/ja/LC_MESSAGES/django.mo @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ffdd50c364a510b4f5cfe7922a5f1a604e8bc7b03aa43ece1dff0250ccde6d6 -size 160575 +oid sha256:47cea504e9acfdcc94a45f2ea96216dddde065d134a92a0ef3e92815a12df6fc +size 162024 diff --git a/apps/locale/ja/LC_MESSAGES/django.po b/apps/locale/ja/LC_MESSAGES/django.po index aa30a95b7..1c40efb67 100644 --- a/apps/locale/ja/LC_MESSAGES/django.po +++ b/apps/locale/ja/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-19 15:41+0800\n" +"POT-Creation-Date: 2023-09-25 16:22+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME{% trans 'Username' %}: [{{ username }}]
+{% trans 'Assets' %}: [{{ asset }}]
+{% trans 'The user has just successfully logged into the asset. Please ensure that this is an authorized operation. If you suspect that this is an unauthorized access, please take appropriate measures immediately.' %}
+ +{% trans 'Thank you' %}!
+ diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index 2c1b0d0f1..5b9481da0 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -15,6 +15,7 @@ from rest_framework.request import Request from rest_framework.response import Response from accounts.const import AliasAccount +from acls.notifications import AssetLoginReminderMsg from common.api import JMSModelViewSet from common.exceptions import JMSException from common.utils import random_string, get_logger, get_request_ip @@ -409,6 +410,10 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView assignees=acl.reviewers.all(), org_id=asset.org_id ) return ticket + if acl.is_action(acl.ActionChoices.notice): + reviewers = acl.reviewers.all() + for reviewer in reviewers: + AssetLoginReminderMsg(reviewer, asset, user).publish_async() class SuperConnectionTokenViewSet(ConnectionTokenViewSet): From 68707085fa0b5ab87d84e51703de69fcdbea751a Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Sat, 7 Oct 2023 17:41:45 +0800 Subject: [PATCH 029/114] =?UTF-8?q?perf:=20=E6=97=A5=E5=BF=97=E4=BF=9D?= =?UTF-8?q?=E5=AD=98=E6=97=B6=E9=97=B4=E4=B8=8D=E5=B0=91=E4=BA=8E6?= =?UTF-8?q?=E4=B8=AA=E6=9C=88=20(#11742)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: feng <1304903146@qq.com> --- apps/jumpserver/settings/custom.py | 15 +++++++++++++-- apps/settings/serializers/cleaning.py | 25 +++++++++++++++++++++---- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py index 18d0bccca..6733faa92 100644 --- a/apps/jumpserver/settings/custom.py +++ b/apps/jumpserver/settings/custom.py @@ -124,6 +124,8 @@ TASK_LOG_KEEP_DAYS = CONFIG.TASK_LOG_KEEP_DAYS OPERATE_LOG_KEEP_DAYS = CONFIG.OPERATE_LOG_KEEP_DAYS ACTIVITY_LOG_KEEP_DAYS = CONFIG.ACTIVITY_LOG_KEEP_DAYS FTP_LOG_KEEP_DAYS = CONFIG.FTP_LOG_KEEP_DAYS +CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS = CONFIG.CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS + ORG_CHANGE_TO_URL = CONFIG.ORG_CHANGE_TO_URL WINDOWS_SKIP_ALL_MANUAL_PASSWORD = CONFIG.WINDOWS_SKIP_ALL_MANUAL_PASSWORD @@ -159,8 +161,6 @@ SECURITY_SESSION_SHARE = CONFIG.SECURITY_SESSION_SHARE LOGIN_REDIRECT_TO_BACKEND = CONFIG.LOGIN_REDIRECT_TO_BACKEND LOGIN_REDIRECT_MSG_ENABLED = CONFIG.LOGIN_REDIRECT_MSG_ENABLED -CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS = CONFIG.CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS - TERMINAL_RAZOR_ENABLED = CONFIG.TERMINAL_RAZOR_ENABLED TERMINAL_OMNIDB_ENABLED = CONFIG.TERMINAL_OMNIDB_ENABLED TERMINAL_MAGNUS_ENABLED = CONFIG.TERMINAL_MAGNUS_ENABLED @@ -204,3 +204,14 @@ MAX_LIMIT_PER_PAGE = CONFIG.MAX_LIMIT_PER_PAGE # Magnus DB Port MAGNUS_ORACLE_PORTS = CONFIG.MAGNUS_ORACLE_PORTS LIMIT_SUPER_PRIV = CONFIG.LIMIT_SUPER_PRIV + +LOG_NAMES = [ + 'LOGIN_LOG_KEEP_DAYS', 'TASK_LOG_KEEP_DAYS', + 'OPERATE_LOG_KEEP_DAYS', 'FTP_LOG_KEEP_DAYS', + 'CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS', + 'ACTIVITY_LOG_KEEP_DAYS', 'TERMINAL_SESSION_KEEP_DURATION' +] +if LIMIT_SUPER_PRIV: + for name in LOG_NAMES: + if globals()[name] < 180: + globals()[name] = 180 diff --git a/apps/settings/serializers/cleaning.py b/apps/settings/serializers/cleaning.py index eb1841561..03fda1f7b 100644 --- a/apps/settings/serializers/cleaning.py +++ b/apps/settings/serializers/cleaning.py @@ -1,5 +1,7 @@ +from django.conf import settings from django.utils.translation import gettext_lazy as _ from rest_framework import serializers +from rest_framework.serializers import ValidationError __all__ = ['CleaningSerializer'] @@ -27,12 +29,27 @@ class CleaningSerializer(serializers.Serializer): min_value=1, max_value=9999, label=_("Cloud sync record keep days (day)"), ) + ACTIVITY_LOG_KEEP_DAYS = serializers.IntegerField( + min_value=1, max_value=9999, + label=_("Activity log keep days (day)"), + ) TERMINAL_SESSION_KEEP_DURATION = serializers.IntegerField( min_value=1, max_value=99999, required=True, label=_('Session keep duration (day)'), help_text=_( 'Session, record, command will be delete if more than duration, only in database, OSS will not be affected.') ) - ACTIVITY_LOG_KEEP_DAYS = serializers.IntegerField( - min_value=1, max_value=9999, - label=_("Activity log keep days (day)"), - ) + MIN_DAYS_THRESHOLD = 180 + + def validate(self, attrs): + attrs = super().validate(attrs) + if not settings.LIMIT_SUPER_PRIV: + return attrs + + error_names = [ + name for name in settings.LOG_NAMES + if attrs.get(name, 0) < self.MIN_DAYS_THRESHOLD + ] + if error_names: + error_message = _('must be greater than {} days.').format(self.MIN_DAYS_THRESHOLD) + raise ValidationError({name: error_message for name in error_names}) + return attrs From dd50044b898ceca6a12c59347837028371fd7edf Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Sat, 7 Oct 2023 17:58:42 +0800 Subject: [PATCH 030/114] =?UTF-8?q?perf:=20=E7=BF=BB=E8=AF=91=20(#11748)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: feng <1304903146@qq.com> --- apps/locale/ja/LC_MESSAGES/django.mo | 4 +- apps/locale/ja/LC_MESSAGES/django.po | 556 ++++++++++++++------------- apps/locale/zh/LC_MESSAGES/django.mo | 4 +- apps/locale/zh/LC_MESSAGES/django.po | 555 +++++++++++++------------- 4 files changed, 571 insertions(+), 548 deletions(-) diff --git a/apps/locale/ja/LC_MESSAGES/django.mo b/apps/locale/ja/LC_MESSAGES/django.mo index 8e58c4d50..c0640b1af 100644 --- a/apps/locale/ja/LC_MESSAGES/django.mo +++ b/apps/locale/ja/LC_MESSAGES/django.mo @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:47cea504e9acfdcc94a45f2ea96216dddde065d134a92a0ef3e92815a12df6fc -size 162024 +oid sha256:56db214d34b91e9f6d712cef124890264144b35b99e50d6833b4af0e935778b0 +size 162655 diff --git a/apps/locale/ja/LC_MESSAGES/django.po b/apps/locale/ja/LC_MESSAGES/django.po index 0a737c38d..0dcc50186 100644 --- a/apps/locale/ja/LC_MESSAGES/django.po +++ b/apps/locale/ja/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-10-07 13:50+0800\n" +"POT-Creation-Date: 2023-10-07 17:56+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME{% trans 'Username' %}: [{{ username }}]
{% trans 'Assets' %}: [{{ asset }}]
+{% trans 'Account' %}: [{{ account }}]
{% trans 'The user has just successfully logged into the asset. Please ensure that this is an authorized operation. If you suspect that this is an unauthorized access, please take appropriate measures immediately.' %}
diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index b0756d4aa..6eb371827 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -419,9 +419,10 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView reviewers = acl.reviewers.all() if not reviewers: return + account_username = account.username self._record_operate_log(acl, asset) for reviewer in reviewers: - AssetLoginReminderMsg(reviewer, asset, user).publish_async() + AssetLoginReminderMsg(reviewer, asset, user, account_username).publish_async() class SuperConnectionTokenViewSet(ConnectionTokenViewSet): From 7eb497f9d30617f029c04c8cc5f75798e08a3e66 Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Thu, 12 Oct 2023 20:13:21 +0800 Subject: [PATCH 068/114] =?UTF-8?q?fix:=20=E8=B5=84=E4=BA=A7=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E8=A2=AB=E9=99=90=E5=88=B6=EF=BC=8C=E6=B2=A1=E6=9C=89?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E5=88=B0=E5=BD=93=E5=89=8D=E7=BB=84=E7=BB=87?= =?UTF-8?q?=E7=9A=84=E6=93=8D=E4=BD=9C=E6=97=A5=E5=BF=97=EF=BC=8C=E8=80=8C?= =?UTF-8?q?=E6=98=AF=E8=AE=B0=E5=BD=95=E5=88=B0=E5=85=A8=E5=B1=80=E7=BB=84?= =?UTF-8?q?=E7=BB=87=20(#11827)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: feng <1304903146@qq.com> --- apps/authentication/api/connection_token.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index 6eb371827..f51351b67 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -22,6 +22,7 @@ from common.utils import random_string, get_logger, get_request_ip from common.utils.django import get_request_os from common.utils.http import is_true, is_false from orgs.mixins.api import RootOrgViewMixin +from orgs.utils import tmp_to_org from perms.models import ActionChoices from terminal.connect_methods import NativeClient, ConnectMethodUtil from terminal.models import EndpointRule, Endpoint @@ -383,13 +384,14 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView @staticmethod def _record_operate_log(acl, asset): from audits.handler import create_or_update_operate_log - after = {str(_('Assets')): str(asset)} - object_name = acl._meta.object_name - resource_type = acl._meta.verbose_name - create_or_update_operate_log( - acl.action, resource_type, resource=acl, - after=after, object_name=object_name - ) + with tmp_to_org(asset.org_id): + after = {str(_('Assets')): str(asset)} + object_name = acl._meta.object_name + resource_type = acl._meta.verbose_name + create_or_update_operate_log( + acl.action, resource_type, resource=acl, + after=after, object_name=object_name + ) def _validate_acl(self, user, asset, account): from acls.models import LoginAssetACL From 452ee1224cc364a01d27d7b084680560afe562b4 Mon Sep 17 00:00:00 2001 From: ibuler{% trans 'Account' %}: [{{ account }}]
{% trans 'The user has just successfully logged into the asset. Please ensure that this is an authorized operation. If you suspect that this is an unauthorized access, please take appropriate measures immediately.' %}
+{% trans 'The user has just logged in to the asset. Please ensure that this is an authorized operation. If you suspect that this is an unauthorized access, please take appropriate measures immediately.' %}
{% trans 'Thank you' %}!
diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index f51351b67..c14df034d 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -361,9 +361,10 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView if account.has_secret: data['input_secret'] = '' + input_username = data.get('input_username', '') if account.username != AliasAccount.INPUT: data['input_username'] = '' - ticket = self._validate_acl(user, asset, account) + ticket = self._validate_acl(user, asset, account, input_username) if ticket: data['from_ticket'] = ticket data['is_active'] = False @@ -382,10 +383,13 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView return account @staticmethod - def _record_operate_log(acl, asset): + def _record_operate_log(acl, asset, input_username): from audits.handler import create_or_update_operate_log with tmp_to_org(asset.org_id): - after = {str(_('Assets')): str(asset)} + after = { + str(_('Assets')): str(asset), + str(_('Account')): input_username + } object_name = acl._meta.object_name resource_type = acl._meta.verbose_name create_or_update_operate_log( @@ -393,7 +397,7 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView after=after, object_name=object_name ) - def _validate_acl(self, user, asset, account): + def _validate_acl(self, user, asset, account, input_username): from acls.models import LoginAssetACL acls = LoginAssetACL.filter_queryset(user=user, asset=asset, account=account) ip = get_request_ip(self.request) @@ -401,19 +405,19 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView if not acl: return if acl.is_action(acl.ActionChoices.accept): - self._record_operate_log(acl, asset) + self._record_operate_log(acl, asset, input_username) return if acl.is_action(acl.ActionChoices.reject): - self._record_operate_log(acl, asset) + self._record_operate_log(acl, asset, input_username) msg = _('ACL action is reject: {}({})'.format(acl.name, acl.id)) raise JMSException(code='acl_reject', detail=msg) if acl.is_action(acl.ActionChoices.review): if not self.request.query_params.get('create_ticket'): msg = _('ACL action is review') raise JMSException(code='acl_review', detail=msg) - self._record_operate_log(acl, asset) + self._record_operate_log(acl, asset, input_username) ticket = LoginAssetACL.create_login_asset_review_ticket( - user=user, asset=asset, account_username=account.username, + user=user, asset=asset, account_username=input_username, assignees=acl.reviewers.all(), org_id=asset.org_id ) return ticket @@ -421,10 +425,12 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView reviewers = acl.reviewers.all() if not reviewers: return - account_username = account.username - self._record_operate_log(acl, asset) + + self._record_operate_log(acl, asset, input_username) for reviewer in reviewers: - AssetLoginReminderMsg(reviewer, asset, user, account_username).publish_async() + AssetLoginReminderMsg( + reviewer, asset, user, input_username + ).publish_async() class SuperConnectionTokenViewSet(ConnectionTokenViewSet): diff --git a/apps/locale/ja/LC_MESSAGES/django.mo b/apps/locale/ja/LC_MESSAGES/django.mo index c1648acb8..50a9d1152 100644 --- a/apps/locale/ja/LC_MESSAGES/django.mo +++ b/apps/locale/ja/LC_MESSAGES/django.mo @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:38bd8a6653f3f4dc63552b1c86379f82067f9f9daac227bacb3af3f9f62134f9 -size 161704 +oid sha256:d7366743ab156daec72625cd241f76420c551c6a48330266a57e3d0b808d4164 +size 162675 diff --git a/apps/locale/ja/LC_MESSAGES/django.po b/apps/locale/ja/LC_MESSAGES/django.po index 4ba81a5a4..f3fa1ddb4 100644 --- a/apps/locale/ja/LC_MESSAGES/django.po +++ b/apps/locale/ja/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-10-12 09:51+0800\n" +"POT-Creation-Date: 2023-10-13 15:03+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAMEM0P}Sfy&FQBC
z-NM&zg9Cgo4|mX^ig-;?%vfW_Y?Bx?7B@Q54*@y(;$$nuR~lQ#FY#z$lzCMwN25C9
z0eF2yYIZ_nX4@*vdvoc5yuEN=Ml@HTmo?`B`X%I3{p1mB+HBJGV>z7UPRzlXePpv(
z8&Xq@L$Z-Zv_Db(e?BA9G>3FmpY06fbItsS9`gF%pFxIn?~D%s(xUKPoYHQ?>yMF6
zMwU-tYJ@tyT(X=+TW{!n^uq>FW>|<0A5N~-?lAkr HB1VeIw%z)w6oQF1m
zi$r@S$kZCgMSrSb7<%y2Q1+fZf^z@RGEaWFn}MKHFUy`!l(GC0&zW4;s|8@y?b4Ie
zI8SW4i#Sl1KcRC7_;s*sS9r!$&Elf5NPpR(TWule%D(N4f RNpMUfvq}?zwbGa;CsV
zkUJh1!FeYJCA%5s@+NJVbDQH$JqfeZGNk5_v=E3DN5NXy!=cF{j>HZY5o#kr4vPH|
zz%|(&?evR}AQU{U9(Z w-f7=~G1?mS%$Yfz8CcR-0Eik96KJQ|hgtPGJFU|PosE}lCfaqr77PDL4gpAkbMeaX)xHgdc)YJa3UDw1s5i??b
z>Tu*X;;4+47HP?fTfNhn6)zI}gsHhjK>4^=P~&c%T8Lqw6q*NM;6Ho#nor~utsqo8
zW^^<v=m~nECihtU^^Qg_oQ7`E3%0JrtyDmFh|oI)$