Projet

Général

Profil

0006-api-add-keepalive-option-to-user-syncronization-API-.patch

Benjamin Dauvergne, 10 octobre 2022 10:37

Télécharger (10,7 ko)

Voir les différences:

Subject: [PATCH 6/6] api: add keepalive option to user syncronization API
 (#67901)

 src/authentic2/api_views.py            | 43 +++++++++++++++-
 src/authentic2/authentication.py       |  3 ++
 src/authentic2/journal_event_types.py  | 36 ++++++++++++++
 tests/api/test_user_synchronization.py | 68 +++++++++++++++++++++++++-
 4 files changed, 147 insertions(+), 3 deletions(-)
src/authentic2/api_views.py
14 14
# You should have received a copy of the GNU Affero General Public License
15 15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 16

  
17
import datetime
17 18
import logging
18 19
import smtplib
19 20
from functools import partial
......
25 26
from django.contrib.auth.hashers import identify_hasher
26 27
from django.contrib.contenttypes.models import ContentType
27 28
from django.core.exceptions import MultipleObjectsReturned
28
from django.db import models
29
from django.db import models, transaction
29 30
from django.shortcuts import get_object_or_404
30 31
from django.utils.dateparse import parse_datetime
31 32
from django.utils.encoding import force_str
32 33
from django.utils.text import slugify
34
from django.utils.timezone import now
33 35
from django.utils.translation import gettext_lazy as _
34 36
from django.views.decorators.cache import cache_control
35 37
from django.views.decorators.vary import vary_on_headers
......
52 54
from rest_framework.views import APIView
53 55
from rest_framework.viewsets import ModelViewSet, ViewSet
54 56

  
57
from authentic2.apps.journal.journal import journal
55 58
from authentic2.apps.journal.models import reference_integer
56 59
from authentic2.compat.drf import action
57 60

  
......
796 799
        known_uuids = serializers.ListField(child=serializers.CharField())
797 800
        full_known_users = serializers.BooleanField(required=False)
798 801
        timestamp = serializers.DateTimeField(required=False)
802
        keepalive = serializers.BooleanField(required=False)
799 803

  
800 804
    def check_unknown_uuids(self, remote_uuids, users):
801 805
        return set(remote_uuids) - {user.uuid for user in users}
......
843 847
            # reload users to get all fields
844 848
            known_users = User.objects.filter(pk__in=[user.pk for user in users[:1000]])
845 849
            data['known_users'] = [BaseUserSerializer(user).data for user in known_users]
850
        # update keepalive if requested and:
851
        # - user is an administrator of users,
852
        # - user is a publik service using publik signature.
853
        # It currently excludes APIClient and OIDCClient
854
        keepalive = serializer.validated_data.get('keepalive', False)
855
        if keepalive:
856
            if not (
857
                str(request.user).startswith('Publik Service')
858
                or (isinstance(request.user, User) and request.user.has_perm('custom_user.admin_user'))
859
            ):
860
                raise PermissionDenied('keepalive requires the admin_user permission')
861
            self._update_keep_alive(actor=request.user, targeted_users=users)
846 862
        hooks.call_hooks('api_modify_response', self, 'synchronization', data)
847 863
        return Response(data)
848 864

  
865
    def _update_keep_alive(self, actor, targeted_users):
866
        # do not write to db uselessly, one keepalive event by month is ok
867
        start = now()
868
        threshold = start - datetime.timedelta(days=30)
869
        users_to_check = User.objects.filter(
870
            (models.Q(last_login__isnull=True) | models.Q(last_login__lt=threshold))
871
            & (models.Q(keepalive__isnull=True) | models.Q(keepalive__lt=threshold)),
872
            pk__in=targeted_users,
873
        ).select_related('ou')
874
        with transaction.atomic(savepoint=False):
875
            users_pks_to_update = {}
876
            for user in users_to_check:
877
                if user.ou and user.ou.clean_unused_accounts_alert:
878
                    activity_date = max(
879
                        user.date_joined,
880
                        user.last_login or user.date_joined,
881
                        user.keepalive or user.date_joined,
882
                    )
883
                    time_since_last_activity = now() - activity_date
884
                    if time_since_last_activity.days > (user.ou.clean_unused_accounts_alert / 2):
885
                        users_pks_to_update[user.pk] = user
886
            User.objects.filter(pk__in=users_pks_to_update.keys()).update(keepalive=start)
887
            for user in users_pks_to_update.values():
888
                journal.record('user.notification.activity', actor=actor, target_user=user, api=True)
889

  
849 890
    @action(
850 891
        detail=True,
851 892
        methods=['post'],
src/authentic2/authentication.py
51 51
    def is_authenticated(self):
52 52
        return CallableTrue
53 53

  
54
    def __str__(self):
55
        return f'OIDC Client "{self.oidc_client}"'
56

  
54 57

  
55 58
class Authentic2Authentication(BasicAuthentication):
56 59
    def authenticate_credentials(self, userid, password, request=None):
src/authentic2/journal_event_types.py
514 514
            'notification sent to "{email}" after {days_of_inactivity} days of inactivity. '
515 515
            'Account will be deleted in {days_to_deletion} days.'
516 516
        ).format(days_of_inactivity=days_of_inactivity, days_to_deletion=days_to_deletion, email=email)
517

  
518

  
519
class UserNotificationActivity(EventTypeDefinition):
520
    name = 'user.notification.activity'
521
    label = _('user activity notification')
522

  
523
    @classmethod
524
    def record(cls, *, actor, target_user):
525
        user = actor if getattr(actor, 'pk', None) is not None else None
526
        service = user.oidc_client if getattr(user, 'oidc_client', None) is not None else None
527
        data = {
528
            'actor': str(actor),
529
            'target_user': str(target_user),
530
            'target_user_pk': target_user.pk,
531
        }
532
        references = [target_user]
533
        if service is not None:
534
            references.append(service)
535
        if user is not None:
536
            references.append(user)
537
        super().record(user=user, service=service, data=data, references=references)
538

  
539
    @classmethod
540
    def get_message(cls, event, context):
541
        user, actor_service = event.get_typed_references(User, Service)
542
        user, actor_user = event.get_typed_references(User, User)
543
        if actor_service:
544
            actor = _('service "{0}"').format(actor_service)
545
        elif actor_user:
546
            actor = _('user "{0}"').format(actor_user)
547
        else:
548
            actor = _('unknown actor')
549
        if context == user:
550
            return _('user activity notified by {0}').format(actor)
551
        else:
552
            return _('user "{0}" activity notified by {1}').format(user, actor)
tests/api/test_user_synchronization.py
20 20

  
21 21
import pytest
22 22
from django.contrib.contenttypes.models import ContentType
23
from django.db import models
23 24
from django.urls import reverse
25
from django.utils.timezone import now
24 26

  
25
from authentic2.a2_rbac.models import Role
27
from authentic2.a2_rbac.models import Permission, Role
28
from authentic2.a2_rbac.utils import get_default_ou
26 29
from authentic2.apps.journal.models import Event, EventType
27 30
from authentic2.custom_user.models import User
28
from django_rbac.models import SEARCH_OP
31
from django_rbac.models import ADMIN_OP, SEARCH_OP
32
from django_rbac.utils import get_operation
29 33

  
30 34
URL = '/api/users/synchronization/'
31 35

  
......
180 184
    assert len(response.json['unknown_uuids']) == 3
181 185
    for user in users[3:]:
182 186
        assert user.uuid in response.json['unknown_uuids']
187

  
188

  
189
def test_keepalive_false(app, payload, unknown_uuids):
190
    app.post_json(URL, params=payload)
191
    assert User.objects.filter(keepalive__isnull=False).count() == 0
192

  
193
    payload['keepalive'] = False
194
    app.post_json(URL, params=payload)
195
    assert User.objects.filter(keepalive__isnull=False).count() == 0
196

  
197

  
198
def test_keepalive_missing_permission(app, user, payload, freezer):
199
    payload['keepalive'] = True
200
    app.post_json(URL, params=payload, status=403)
201

  
202

  
203
class TestWithPermission:
204
    @pytest.fixture(autouse=True)
205
    def configure_ou(self, users, db):
206
        ou = get_default_ou()
207
        User.objects.all().update(ou=ou)
208
        User.objects.update(last_login=models.F('date_joined'))
209
        ou.clean_unused_accounts_alert = 60
210
        ou.clean_unused_accounts_deletion = 63
211
        ou.save()
212

  
213
    @pytest.fixture
214
    def user(self, user):
215
        perm, _ = Permission.objects.get_or_create(
216
            ou__isnull=True,
217
            operation=get_operation(ADMIN_OP),
218
            target_ct=ContentType.objects.get_for_model(ContentType),
219
            target_id=ContentType.objects.get_for_model(User).pk,
220
        )
221
        user.roles.all()[0].permissions.add(perm)
222
        return user
223

  
224
    @pytest.fixture
225
    def payload(self, payload):
226
        payload['keepalive'] = True
227
        return payload
228

  
229
    def test_keepalive_true(self, app, user, users, payload, freezer):
230
        freezer.move_to(datetime.timedelta(days=50, hours=1))
231
        app.post_json(URL, params=payload)
232
        assert User.objects.filter(keepalive__isnull=False).count() == len(users)
233

  
234
    def test_keepalive_one_time_by_clean_unused_period_alert(self, app, user, users, payload, freezer):
235
        # set last keepalive 29 days ago
236
        User.objects.exclude(pk=user.pk).update(keepalive=now())
237
        app.post_json(URL, params=payload)
238
        freezer.move_to(datetime.timedelta(days=30))
239
        # keepalive did not change
240
        assert User.objects.filter(keepalive__lt=now() - datetime.timedelta(days=1)).count() == 10
241

  
242
        # move 2 days in the future
243
        freezer.move_to(datetime.timedelta(days=1))
244
        app.post_json(URL, params=payload)
245
        # keepalive did change
246
        assert User.objects.filter(keepalive__lt=now() - datetime.timedelta(days=1)).count() == 0
183
-