Projet

Général

Profil

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

Benjamin Dauvergne, 02 novembre 2022 14:22

Télécharger (12,6 ko)

Voir les différences:

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

 src/authentic2/api_views.py            | 33 ++++++++++++-
 src/authentic2/authentication.py       |  3 ++
 src/authentic2/journal_event_types.py  | 33 +++++++++++++
 tests/api/test_user_synchronization.py | 66 +++++++++++++++++++++++++-
 tests/test_manager_journal.py          | 30 +++++++++++-
 5 files changed, 161 insertions(+), 4 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

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

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

  
866
    def _update_keep_alive(self, actor, targeted_users, period_in_days=30):
867
        # do not write to db uselessly, one keepalive event by month is ok
868
        start = now()
869
        threshold = start - datetime.timedelta(days=period_in_days)
870
        users_to_update = User.objects.filter(pk__in=targeted_users).exclude(
871
            models.Q(date_joined__gt=threshold)
872
            | models.Q(last_login__gt=threshold)
873
            | models.Q(keepalive__gt=threshold)
874
        )
875
        with transaction.atomic(savepoint=False):
876
            users_to_update.update(keepalive=start)
877
            actor = actor if isinstance(actor, User) else getattr(actor, 'oidc_client', None)
878
            for user in users_to_update.only('id'):
879
                journal.record('user.notification.activity', actor=actor, target_user=user, api=True)
880

  
850 881
    @action(
851 882
        detail=True,
852 883
        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(EventTypeWithService):
520
    name = 'user.notification.activity'
521
    label = _('user activity notification')
522

  
523
    @classmethod
524
    def record(cls, *, actor, target_user):
525
        user = actor if isinstance(actor, User) else None
526
        service = actor if isinstance(actor, Service) else None
527
        data = {
528
            'target_user': str(target_user),
529
            'target_user_pk': target_user.pk,
530
        }
531
        references = [target_user]
532
        super().record(user=user, service=service, data=data, references=references)
533

  
534
    @classmethod
535
    def get_message(cls, event, context):
536
        actor_user = event.user
537
        actor_service, target_user = event.get_typed_references(Service, User)
538
        if actor_service is None:
539
            (target_user,) = event.get_typed_references(User)
540
        if actor_user is not None:
541
            actor = _('user "{0}"').format(actor_user)
542
        elif actor_service:
543
            actor = _('service "{0}"').format(actor_service)
544
        else:
545
            actor = _('unknown actor')
546
        if context == target_user:
547
            return _('user activity notified by {0}').format(actor)
548
        else:
549
            return _('user "{0}" activity notified by {1}').format(target_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 SEARCH_OP, Role
27
from authentic2.a2_rbac.models import ADMIN_OP, SEARCH_OP, 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
31
from django_rbac.utils import get_operation
28 32

  
29 33
URL = '/api/users/synchronization/'
30 34

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

  
187

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

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

  
196

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

  
201

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

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

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

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

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

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

  
45 45

  
46 46
@pytest.fixture(autouse=True)
47
def events(db, freezer):
47
def events(db, superuser, freezer):
48 48
    session1 = Session(session_key="1234")
49 49
    session2 = Session(session_key="abcd")
50 50

  
......
322 322
        session=session2,
323 323
        related_object=set_attribute_action,
324 324
    )
325
    make('user.notification.activity', actor=service, target_user=user)
326
    make('user.notification.activity', actor=superuser, target_user=user)
325 327

  
326 328
    # verify we created at least one event for each type
327 329
    assert set(Event.objects.values_list("type__name", flat=True)) == set(_registry)
......
361 363
    set_attribute_action = SetAttributeAction.objects.get()
362 364

  
363 365
    # remove event about admin login
364
    Event.objects.filter(user=superuser).delete()
366
    Event.objects.order_by('-id').filter(type__name='user.login', user=superuser)[0].delete()
365 367

  
366 368
    response = response.click("Global journal")
367 369

  
......
735 737
            'type': 'authenticator.related_object.deletion',
736 738
            'user': 'agent',
737 739
        },
740
        {
741
            'message': 'user "Johnny doe" activity notified by service "service"',
742
            'timestamp': 'Jan. 3, 2020, 11 a.m.',
743
            'type': 'user.notification.activity',
744
            'user': '-',
745
        },
746
        {
747
            'message': 'user "Johnny doe" activity notified by user "super user"',
748
            'timestamp': 'Jan. 3, 2020, noon',
749
            'type': 'user.notification.activity',
750
            'user': 'super user',
751
        },
738 752
    ]
739 753

  
740 754
    agent_page = response.click('agent', index=1)
......
969 983
            'type': 'user.deletion.inactivity',
970 984
            'user': 'Johnny doe',
971 985
        },
986
        {
987
            'message': 'user activity notified by service "service"',
988
            'timestamp': 'Jan. 3, 2020, 11 a.m.',
989
            'type': 'user.notification.activity',
990
            'user': '-',
991
        },
992
        {
993
            'message': 'user activity notified by user "super user"',
994
            'timestamp': 'Jan. 3, 2020, noon',
995
            'type': 'user.notification.activity',
996
            'user': 'super user',
997
        },
972 998
    ]
973 999

  
974 1000

  
975
-