Project

General

Profile

0001-WIP-clean-unused-accounts-depending-on-OUs-26909.patch

Paul Marillonnet, 27 Mar 2019 04:49 PM

Download (10.3 KB)

View differences:

Subject: [PATCH] WIP clean unused accounts depending on OUs (#26909)

 src/authentic2/a2_rbac/models.py              |  19 +++
 src/authentic2/custom_user/models.py          |   2 +
 .../commands/clean-unused-accounts.py         | 123 ++++++++----------
 3 files changed, 74 insertions(+), 70 deletions(-)
src/authentic2/a2_rbac/models.py
1
import os
2

  
1 3
from collections import namedtuple
2 4
from django.core.exceptions import ValidationError
3 5
from django.utils.translation import ugettext_lazy as _
......
76 78
        choices=USER_ADD_PASSWD_POLICY_CHOICES,
77 79
        default=0)
78 80

  
81
    clean_unused_accounts_alert = models.IntegerField(
82
        verbose_name=_('Days after which the user receives an account deletion '
83
            'alert'),
84
        default=0)
85

  
86
    clean_unused_accounts_deletion = models.IntegerField(
87
        verbose_name=_('Delay in days before cleaning unused accounts'),
88
        default=0)
89

  
90
    clean_unused_accounts_from_email = models.EmailField(
91
        verbose_name=_('sender email address for account deletion warnings and notifications'),
92
        blank=True, default=None, max_length=255)
93

  
94
    clean_unused_accounts_filter = models.CharField(
95
        verbose_name=_('json dict filter to apply for unused account cleanup'),
96
        blank=True, default=None, max_length=511)
97

  
79 98
    objects = managers.OrganizationalUnitManager()
80 99

  
81 100
    class Meta:
src/authentic2/custom_user/models.py
126 126
        verbose_name=_('Last modification time'),
127 127
        db_index=True,
128 128
        auto_now=True)
129
    last_account_deletion_alert = models.DateTimeField(
130
        _('last account deletion alert'), default=None)
129 131

  
130 132
    objects = UserManager.from_queryset(UserQuerySet)()
131 133
    attributes = AttributesDescriptor()
src/authentic2/management/commands/clean-unused-accounts.py
1 1
from __future__ import print_function
2 2

  
3
import json
3 4
import logging
4
import datetime
5 5

  
6
from datetime import timedelta
6 7
from django.contrib.auth import get_user_model
7 8
from django.core.management.base import BaseCommand, CommandError
8 9
from django.core.mail import send_mail
9 10
from django.utils.timezone import now
10 11
from django.template.loader import render_to_string
12
from django_rbac.utils import get_ou_model
11 13

  
12 14
from authentic2.models import DeletedUser
13 15

  
......
24 26
    help = '''Clean unused accounts'''
25 27

  
26 28
    def add_arguments(self, parser):
27
        parser.add_argument('clean_threshold', type=int)
28
        parser.add_argument(
29
            "--alert-thresholds",
30
            help='list of durations before sending an alert '
31
                 'message for unused account, default is none',
32
            default=None)
33
        parser.add_argument(
34
            "--period", type=int,
35
            help='period between two calls to '
36
                 'clean-unused-accounts as days, default is 1',
37
            default=1
38
        )
39 29
        parser.add_argument("--fake", action='store_true', help='do nothing', default=False)
40
        parser.add_argument(
41
            "--filter", help='filter to apply to the user queryset, '
42
            'the Django filter key and value are separated by character =', action='append',
43
            default=[]
44
        )
45
        parser.add_argument(
46
            '--from-email', default=settings.DEFAULT_FROM_EMAIL,
47
            help='sender address for notifications, default is DEFAULT_FROM_EMAIL from settings'
48
        )
49 30

  
50 31
    def handle(self, *args, **options):
51 32
        log = logging.getLogger(__name__)
......
55 36
            log.exception('failure while cleaning unused accounts')
56 37

  
57 38
    def clean_unused_acccounts(self, *args, **options):
58
        if options['period'] < 1:
59
            raise CommandError('period must be > 0')
60

  
61
        clean_threshold = options['clean_threshold']
62
        if clean_threshold < 1:
63
            raise CommandError('clean_threshold must be an integer > 0')
64

  
65 39
        if options['verbosity'] == '0':
66 40
            logging.basicConfig(level=logging.CRITICAL)
67 41
        if options['verbosity'] == '1':
......
74 48
        log = logging.getLogger(__name__)
75 49
        n = now().replace(hour=0, minute=0, second=0, microsecond=0)
76 50
        self.fake = options['fake']
77
        self.from_email = options['from_email']
78 51
        if self.fake:
79 52
            log.info('fake call to clean-unused-accounts')
80 53
        users = get_user_model().objects.all()
81
        if options['filter']:
82
            for f in options['filter']:
83
                key, value = f.split('=', 1)
54
        for ou in get_ou_model().objects.all():
55
            if ou.clean_unused_accounts_filter:
84 56
                try:
85
                    users = users.filter(**{key: value})
57
                    for key, value in json.loads(ou.clean_unused_accounts_filter):
58
                        users = users.filter(**{key: value})
86 59
                except:
87
                    raise CommandError('invalid --filter %s' % f)
88
        if options['alert_thresholds']:
89
            alert_thresholds = options['alert_thresholds']
90
            alert_thresholds = alert_thresholds.split(',')
91
            try:
92
                alert_thresholds = map(int, alert_thresholds)
93
            except ValueError:
94
                raise CommandError('alert_thresholds must be a comma '
95
                        'separated list of integers')
96
            for threshold in alert_thresholds:
97
                if not (0 < threshold < clean_threshold):
98
                    raise CommandError('alert-threshold must a positive integer '
99
                            'inferior to clean-threshold: 0 < %d < %d' % (
100
                                threshold, clean_threshold))
101
            for threshold in alert_thresholds:
102
                a = n - datetime.timedelta(days=threshold)
103
                b = n - datetime.timedelta(days=threshold-options['period'])
104
                for user in users.filter(last_login__lt=b, last_login__gte=a):
105
                    log.info('%s last login %d days ago, sending alert', user, threshold)
106
                    self.send_alert(user, threshold, clean_threshold-threshold)
107
        threshold = n - datetime.timedelta(days=clean_threshold)
108
        for user in users.filter(last_login__lt=threshold):
109
            d = n - user.last_login
110
            log.info('%s last login %d days ago, deleting user', user, d.days)
111
            self.delete_user(user, clean_threshold)
112

  
113

  
114
    def send_alert(self, user, threshold, clean_threshold):
115
        ctx = { 'user': user, 'threshold': threshold,
116
                'clean_threshold': clean_threshold }
117
        self.send_mail('authentic2/unused_account_alert', user, ctx)
60
                    raise CommandError('invalid filter {}: {}'.format(
61
                        key, value))
62
            if not ou.clean_unused_accounts_deletion:
63
                continue
64

  
65
            deletion = timedelta(days=ou.clean_unused_accounts_deletion)
66
            alert = timedelta(days=ou.clean_unused_accounts_alert)
67

  
68
            for user in users.filter(ou=ou, last_login_lt=n-alert):
69
                if n - user.last_login >= deletion and \
70
                        user.last_account_deletion_alert and \
71
                        n - user.last_account_deletion_alert >= alert:
72
                    log.info(
73
                        '%s last login more than %d days ago, deleting user',
74
                        user, ou.clean_unused_accounts_deletion)
75
                    self.delete_user(user, clean_threshold)
76
                elif not user.last_account_deletion_alert or \
77
                        n - user.last_account_deletion_alert >= alert:
78
                    log.info('%s last login %d days ago, sending alert', user,
79
                        ou.clean_unused_accounts_alert)
80
                    self.send_alert(user)
81
                    user.last_account_deletion_alert = n
82
                    user.save()
83

  
84

  
85
    def send_alert(self, user):
86
        ctx = {
87
            'user': user,
88
            'threshold': user.ou.clean_unused_accounts_alert,
89
            'clean_threshold': user.ou.clean_unused_accounts_deletion
90
        }
91
        self.send_mail(
92
                'authentic2/unused_account_alert_{}'.format(user.ou.slug),
93
                'authentic2/unused_account_alert', #default fallback template
94
                user, ctx)
118 95

  
119 96

  
120 97
    def send_mail(self, prefix, user, ctx):
......
124 101
            log.debug('%s has no email, no mail sent', user)
125 102
        subject = render_to_string(prefix + '_subject.txt', ctx).strip()
126 103
        body = render_to_string(prefix + '_body.txt', ctx)
104
        from_email = user.ou.clean_unused_accounts_from_email
127 105
        if not self.fake:
128 106
            try:
129 107
                log.debug('sending mail to %s', user.email)
130
                send_mail(subject, body, self.from_email, [user.email])
108
                send_mail(subject, body, from_email, [user.email])
131 109
            except:
132 110
                log.exception('email sending failure')
133 111

  
134 112

  
135
    def delete_user(self, user, threshold):
136
        ctx = { 'user': user, 'threshold': threshold }
137
        self.send_mail('authentic2/unused_account_delete', user,
138
                ctx)
113
    def delete_user(self, user):
114
        ctx = {
115
            'user': user,
116
            'threshold': user.ou.clean_unused_accounts_deletion
117
        }
118
        self.send_mail(
119
                'authentic2/unused_account_delete_{}'.format(user.ou.slug),
120
                'authentic2/unused_account_delete', #default fallback template
121
                user, ctx)
139 122
        if not self.fake:
140 123
            DeletedUser.objects.delete_user(user)
141
-