management: send sms alert to email-less inactive users (#85235)
gitea/authentic/pipeline/head This commit looks good
Details
gitea/authentic/pipeline/head This commit looks good
Details
This commit is contained in:
parent
ad8452b101
commit
836769345d
|
@ -312,15 +312,21 @@ class UserDeletionForInactivity(EventTypeWithService):
|
|||
|
||||
@classmethod
|
||||
def record(cls, *, user, days_of_inactivity):
|
||||
super().record(user=user, data={'days_of_inactivity': days_of_inactivity, 'email': user.email})
|
||||
super().record(
|
||||
user=user,
|
||||
data={
|
||||
'days_of_inactivity': days_of_inactivity,
|
||||
'identifier': user.email or user.phone_identifier,
|
||||
},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_message(cls, event, context):
|
||||
days_of_inactivity = event.get_data('days_of_inactivity')
|
||||
email = event.get_data('email')
|
||||
identifier = event.get_data('identifier')
|
||||
return _(
|
||||
'user deletion after {days_of_inactivity} days of inactivity, notification sent to "{email}".'
|
||||
).format(days_of_inactivity=days_of_inactivity, email=email)
|
||||
'user deletion after {days_of_inactivity} days of inactivity, notification sent to "{identifier}".'
|
||||
).format(days_of_inactivity=days_of_inactivity, identifier=identifier)
|
||||
|
||||
|
||||
class UserServiceSSO(EventTypeWithHow):
|
||||
|
@ -553,11 +559,10 @@ class UserNotificationInactivity(EventTypeDefinition):
|
|||
|
||||
@classmethod
|
||||
def record(cls, *, user, days_of_inactivity, days_to_deletion):
|
||||
assert user.email
|
||||
data = {
|
||||
'days_of_inactivity': days_of_inactivity,
|
||||
'days_to_deletion': days_to_deletion,
|
||||
'email': user.email,
|
||||
'identifier': user.email or user.phone_identifier,
|
||||
}
|
||||
super().record(user=user, data=data)
|
||||
|
||||
|
@ -565,11 +570,13 @@ class UserNotificationInactivity(EventTypeDefinition):
|
|||
def get_message(cls, event, context):
|
||||
days_of_inactivity = event.get_data('days_of_inactivity')
|
||||
days_to_deletion = event.get_data('days_to_deletion')
|
||||
email = event.get_data('email')
|
||||
identifier = event.get_data('identifier')
|
||||
return _(
|
||||
'notification sent to "{email}" after {days_of_inactivity} days of inactivity. '
|
||||
'notification sent to "{identifier}" after {days_of_inactivity} days of inactivity. '
|
||||
'Account will be deleted in {days_to_deletion} days.'
|
||||
).format(days_of_inactivity=days_of_inactivity, days_to_deletion=days_to_deletion, email=email)
|
||||
).format(
|
||||
days_of_inactivity=days_of_inactivity, days_to_deletion=days_to_deletion, identifier=identifier
|
||||
)
|
||||
|
||||
|
||||
class UserNotificationActivity(EventTypeWithService):
|
||||
|
|
|
@ -31,7 +31,8 @@ from authentic2.a2_rbac.models import OrganizationalUnit
|
|||
from authentic2.backends import get_user_queryset
|
||||
from authentic2.backends.ldap_backend import LDAPBackend
|
||||
from authentic2.journal_event_types import UserDeletionForInactivity, UserNotificationInactivity
|
||||
from authentic2.utils.misc import send_templated_mail
|
||||
from authentic2.utils import sms as utils_sms
|
||||
from authentic2.utils.misc import get_password_authenticator, send_templated_mail
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -48,6 +49,10 @@ class Command(BaseCommand):
|
|||
3: logging.DEBUG,
|
||||
}
|
||||
|
||||
def __init__(self, stdout=None, stderr=None, no_color=False, force_color=False):
|
||||
super().__init__(stdout=stdout, stderr=stderr, no_color=no_color, force_color=force_color)
|
||||
self.is_phone_authn_active = get_password_authenticator().is_phone_authn_active
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--fake', action='store_true', help='do nothing', default=False)
|
||||
|
||||
|
@ -65,7 +70,9 @@ class Command(BaseCommand):
|
|||
self.now = timezone.now()
|
||||
|
||||
realms = [block['realm'] for block in LDAPBackend.get_config() if block.get('realm')]
|
||||
self.user_qs = get_user_queryset().exclude(email='').exclude(userexternalid__source__in=realms)
|
||||
self.user_qs = get_user_queryset().exclude(userexternalid__source__in=realms)
|
||||
if not self.is_phone_authn_active:
|
||||
self.user_qs = self.user_qs.exclude(email='')
|
||||
|
||||
translation.activate(settings.LANGUAGE_CODE)
|
||||
try:
|
||||
|
@ -105,7 +112,11 @@ class Command(BaseCommand):
|
|||
days_to_deletion = ou.clean_unused_accounts_deletion - ou.clean_unused_accounts_alert
|
||||
for user in inactive_users_first_alert[:count]:
|
||||
logger.info('%s last login %d days ago, sending alert', user, ou.clean_unused_accounts_alert)
|
||||
self.send_alert(user, days_to_deletion=days_to_deletion, days_of_inactivity=alert_delay.days)
|
||||
self.send_alert(
|
||||
user,
|
||||
days_to_deletion=days_to_deletion,
|
||||
days_of_inactivity=alert_delay.days,
|
||||
)
|
||||
|
||||
inactive_users_to_delete = inactive_users.filter(
|
||||
(
|
||||
|
@ -132,7 +143,7 @@ class Command(BaseCommand):
|
|||
self.delete_user(
|
||||
user,
|
||||
days_of_inactivity=deletion_delay.days,
|
||||
send_mail=user.last_login
|
||||
send_notification=user.last_login
|
||||
or not (getattr(user, 'oidc_account', None) or has_saml_identifiers),
|
||||
)
|
||||
|
||||
|
@ -148,25 +159,46 @@ class Command(BaseCommand):
|
|||
UserNotificationInactivity.record(
|
||||
user=user, days_of_inactivity=days_of_inactivity, days_to_deletion=days_to_deletion
|
||||
)
|
||||
self.send_mail('authentic2/unused_account_alert', user, ctx)
|
||||
if user.email:
|
||||
self.send_mail('authentic2/unused_account_alert', user, ctx)
|
||||
elif self.is_phone_authn_active and user.phone_identifier:
|
||||
self.send_sms('authentic2/unused_account_alert_sms.txt', user, ctx)
|
||||
else:
|
||||
logger.debug('%s has no email or identifiable phone number, alert was not sent', user)
|
||||
|
||||
def send_mail(self, prefix, user, ctx):
|
||||
if not user.email:
|
||||
logger.debug('%s has no email, no mail sent', user)
|
||||
else:
|
||||
logger.debug('sending mail to %s', user.email)
|
||||
if not self.fake:
|
||||
logger.debug('sending mail to %s', user.email)
|
||||
if not self.fake:
|
||||
|
||||
def send_mail():
|
||||
send_templated_mail(user, prefix, ctx)
|
||||
def send_mail():
|
||||
send_templated_mail(user, prefix, ctx)
|
||||
|
||||
transaction.on_commit(send_mail)
|
||||
transaction.on_commit(send_mail)
|
||||
|
||||
def delete_user(self, user, days_of_inactivity, send_mail=True):
|
||||
def send_sms(self, template_name, user, ctx):
|
||||
logger.debug('sending sms to %s', user.email)
|
||||
if not self.fake:
|
||||
|
||||
def send_sms():
|
||||
utils_sms.send_sms(
|
||||
user.phone_identifier,
|
||||
user.ou,
|
||||
user=user,
|
||||
template_names=(template_name,),
|
||||
context=ctx,
|
||||
kind=None,
|
||||
)
|
||||
|
||||
transaction.on_commit(send_sms)
|
||||
|
||||
def delete_user(self, user, days_of_inactivity, send_notification=True):
|
||||
ctx = {'user': user}
|
||||
with transaction.atomic():
|
||||
if send_mail:
|
||||
self.send_mail('authentic2/unused_account_delete', user, ctx)
|
||||
if send_notification:
|
||||
if user.email:
|
||||
self.send_mail('authentic2/unused_account_delete', user, ctx)
|
||||
elif self.is_phone_authn_active and user.phone_identifier:
|
||||
self.send_sms('authentic2/unused_account_delete_sms.txt', user, ctx)
|
||||
if not self.fake:
|
||||
UserDeletionForInactivity.record(user=user, days_of_inactivity=days_of_inactivity)
|
||||
user.delete()
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
{% load i18n %}{% blocktrans %}Your account is inactive, please log in within {{ days_to_deletion }} days to {{ login_url }} to prevent its deletion.{% endblocktrans %}
|
|
@ -0,0 +1 @@
|
|||
{% load i18n %}{% blocktrans %}Your account was inactive and has therefore been deleted.{% endblocktrans %}
|
|
@ -95,7 +95,7 @@ def test_clean_unused_account(db, simple_user, mailoutbox, freezer, settings):
|
|||
assert len(mailoutbox) == 1
|
||||
assert (
|
||||
Event.objects.filter(
|
||||
type__name='user.notification.inactivity', user=simple_user, data__email=simple_user.email
|
||||
type__name='user.notification.inactivity', user=simple_user, data__identifier=simple_user.email
|
||||
).count()
|
||||
== 1
|
||||
)
|
||||
|
@ -115,7 +115,95 @@ def test_clean_unused_account(db, simple_user, mailoutbox, freezer, settings):
|
|||
assert mailoutbox[-1].to == [email]
|
||||
assert (
|
||||
Event.objects.filter(
|
||||
type__name='user.deletion.inactivity', user=simple_user, data__email=simple_user.email
|
||||
type__name='user.deletion.inactivity', user=simple_user, data__identifier=simple_user.email
|
||||
).count()
|
||||
== 1
|
||||
)
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_clean_unused_account_sms(db, nomail_user, mailoutbox, freezer, settings, phone_activated_authn):
|
||||
settings.LDAP_AUTH_SETTINGS = [{'realm': 'ldap', 'url': 'ldap://ldap.com/', 'basedn': 'dc=ldap,dc=com'}]
|
||||
settings.SMS_URL = 'https://foo.whatever.none/'
|
||||
rsps = responses.post(
|
||||
settings.SMS_URL,
|
||||
json={
|
||||
'headers': {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
'status_code': 200,
|
||||
'data': {},
|
||||
},
|
||||
)
|
||||
ldap_user = User.objects.create(username='ldap-user', ou=nomail_user.ou)
|
||||
ldap_user.attributes.phone = '+33122334455'
|
||||
ldap_user.save()
|
||||
oidc_user = User.objects.create(username='oidc-user', ou=nomail_user.ou)
|
||||
oidc_user.attributes.phone = '+33122334456'
|
||||
oidc_user.save()
|
||||
saml_user = User.objects.create(username='saml-user', ou=nomail_user.ou)
|
||||
saml_user.attributes.phone = '+33122334457'
|
||||
saml_user.save()
|
||||
UserExternalId.objects.create(user=ldap_user, source='ldap', external_id='whatever')
|
||||
provider = OIDCProvider.objects.create(name='oidc', ou=nomail_user.ou)
|
||||
OIDCAccount.objects.create(user=oidc_user, provider=provider, sub='1')
|
||||
|
||||
issuer = Issuer.objects.create(entity_id='https://idp1.example.com/', slug='idp1')
|
||||
UserSAMLIdentifier.objects.create(user=saml_user, issuer=issuer, name_id='1234')
|
||||
|
||||
freezer.move_to('2018-01-01')
|
||||
nomail_user.attributes.phone = '+33611223344'
|
||||
nomail_user.save()
|
||||
nomail_user.ou.clean_unused_accounts_alert = 2
|
||||
nomail_user.ou.clean_unused_accounts_deletion = 3
|
||||
nomail_user.ou.save()
|
||||
|
||||
last_login = now() - datetime.timedelta(days=2, seconds=30)
|
||||
for user in (nomail_user, ldap_user, oidc_user, saml_user):
|
||||
user.last_login = last_login
|
||||
user.save()
|
||||
|
||||
call_command('clean-unused-accounts')
|
||||
assert rsps.call_count == 1
|
||||
assert 'Your account is inactive, please log in' in json.loads(rsps.calls[-1].request.body)['message']
|
||||
# check message contains login url
|
||||
assert 'https://testserver/login/' in json.loads(rsps.calls[-1].request.body)['message']
|
||||
|
||||
assert User.objects.count() == 4
|
||||
assert len(mailoutbox) == 0
|
||||
assert (
|
||||
Event.objects.filter(
|
||||
type__name='user.notification.inactivity',
|
||||
user=nomail_user,
|
||||
data__identifier=nomail_user.attributes.phone,
|
||||
).count()
|
||||
== 1
|
||||
)
|
||||
|
||||
freezer.move_to('2018-01-01 12:00:00')
|
||||
# no new sms, no deletion
|
||||
call_command('clean-unused-accounts')
|
||||
assert rsps.call_count == 1
|
||||
|
||||
assert User.objects.count() == 4
|
||||
assert len(mailoutbox) == 0
|
||||
|
||||
freezer.move_to('2018-01-02')
|
||||
call_command('clean-unused-accounts')
|
||||
assert rsps.call_count == 2
|
||||
assert (
|
||||
'Your account was inactive and has therefore been deleted.'
|
||||
in json.loads(rsps.calls[-1].request.body)['message']
|
||||
)
|
||||
|
||||
assert User.objects.count() == 3
|
||||
deleted_user = DeletedUser.objects.get()
|
||||
assert deleted_user.old_user_id == nomail_user.id
|
||||
assert (
|
||||
Event.objects.filter(
|
||||
type__name='user.deletion.inactivity',
|
||||
user=nomail_user,
|
||||
data__identifier=nomail_user.attributes.phone,
|
||||
).count()
|
||||
== 1
|
||||
)
|
||||
|
@ -257,7 +345,7 @@ def test_clean_unused_account_never_logged_in(app, db, simple_user, mailoutbox,
|
|||
assert len(mailoutbox) == 2
|
||||
assert (
|
||||
Event.objects.filter(
|
||||
type__name='user.deletion.inactivity', user=simple_user, data__email=simple_user.email
|
||||
type__name='user.deletion.inactivity', user=simple_user, data__identifier=simple_user.email
|
||||
).count()
|
||||
== 1
|
||||
)
|
||||
|
@ -310,19 +398,19 @@ def test_clean_unused_federated_account_never_logged_in(app, db, simple_user, ma
|
|||
assert len(mailoutbox) == 0
|
||||
assert (
|
||||
Event.objects.filter(
|
||||
type__name='user.deletion.inactivity', user=simple_user, data__email=simple_user.email
|
||||
type__name='user.deletion.inactivity', user=simple_user, data__identifier=simple_user.email
|
||||
).count()
|
||||
== 1
|
||||
)
|
||||
assert (
|
||||
Event.objects.filter(
|
||||
type__name='user.deletion.inactivity', user=ldap_user, data__email=ldap_user.email
|
||||
type__name='user.deletion.inactivity', user=ldap_user, data__identifier=ldap_user.email
|
||||
).count()
|
||||
== 0
|
||||
)
|
||||
assert (
|
||||
Event.objects.filter(
|
||||
type__name='user.deletion.inactivity', user=saml_user, data__email=saml_user.email
|
||||
type__name='user.deletion.inactivity', user=saml_user, data__identifier=saml_user.email
|
||||
).count()
|
||||
== 1
|
||||
)
|
||||
|
@ -337,7 +425,7 @@ def test_clean_unused_federated_account_never_logged_in(app, db, simple_user, ma
|
|||
assert len(mailoutbox) == 0
|
||||
assert (
|
||||
Event.objects.filter(
|
||||
type__name='user.deletion.inactivity', user=ldap_user, data__email=ldap_user.email
|
||||
type__name='user.deletion.inactivity', user=ldap_user, data__identifier=ldap_user.email
|
||||
).count()
|
||||
== 0
|
||||
)
|
||||
|
@ -381,13 +469,13 @@ def test_clean_unused_federated_account_logged_in_untouched(app, db, simple_user
|
|||
assert len(mailoutbox) == 0
|
||||
assert (
|
||||
Event.objects.filter(
|
||||
type__name='user.deletion.inactivity', user=simple_user, data__email=simple_user.email
|
||||
type__name='user.deletion.inactivity', user=simple_user, data__identifier=simple_user.email
|
||||
).count()
|
||||
== 0
|
||||
)
|
||||
assert (
|
||||
Event.objects.filter(
|
||||
type__name='user.deletion.inactivity', user=saml_user, data__email=saml_user.email
|
||||
type__name='user.deletion.inactivity', user=saml_user, data__identifier=saml_user.email
|
||||
).count()
|
||||
== 0
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue