0001-api-add-timestamp-parameter-to-users-synchronization.patch
src/authentic2/api_views.py | ||
---|---|---|
23 | 23 |
from django.conf import settings |
24 | 24 |
from django.contrib.auth import get_user_model |
25 | 25 |
from django.contrib.auth.hashers import identify_hasher |
26 |
from django.contrib.contenttypes.models import ContentType |
|
26 | 27 |
from django.core.exceptions import MultipleObjectsReturned |
27 | 28 |
from django.db import models |
28 | 29 |
from django.shortcuts import get_object_or_404 |
... | ... | |
57 | 58 |
from . import api_mixins, app_settings, decorators, hooks |
58 | 59 |
from .a2_rbac.models import OrganizationalUnit, Role |
59 | 60 |
from .a2_rbac.utils import get_default_ou |
60 |
from .custom_user.models import User |
|
61 |
from .apps.journal.models import Event |
|
62 |
from .custom_user.models import DeletedUser, User |
|
61 | 63 |
from .journal_event_types import UserLogin, UserRegistration |
62 | 64 |
from .models import Attribute, PasswordReset, Service |
63 | 65 |
from .passwords import get_password_checker |
... | ... | |
795 | 797 |
class SynchronizationSerializer(serializers.Serializer): |
796 | 798 |
known_uuids = serializers.ListField(child=serializers.CharField()) |
797 | 799 |
full_known_users = serializers.BooleanField(required=False) |
800 |
timestamp = serializers.DateTimeField(required=False) |
|
798 | 801 | |
799 |
def check_uuids(self, uuids): |
|
802 |
def check_uuids(self, uuids, timestamp=None):
|
|
800 | 803 |
User = get_user_model() |
801 | 804 |
known_uuids = User.objects.filter(uuid__in=uuids).values_list('uuid', flat=True) |
802 |
return (known_uuids, set(uuids) - set(known_uuids)) |
|
805 |
unknown_uuids = set(uuids) - set(known_uuids) |
|
806 |
modified_users_uuids = [] |
|
807 |
if timestamp: |
|
808 |
unknown_uuids = DeletedUser.objects.filter( |
|
809 |
old_uuid__in=unknown_uuids, deleted__gt=timestamp |
|
810 |
).values_list('old_uuid', flat=True) |
|
811 |
user_ct = ContentType.objects.get_for_model(get_user_model()) |
|
812 |
user_events = Event.objects.filter( |
|
813 |
reference_ct_ids__contains=[user_ct.id], timestamp__gt=timestamp |
|
814 |
) |
|
815 |
for user_event in user_events: |
|
816 |
for reference in user_event.references: |
|
817 |
if ( |
|
818 |
reference is not None # xxx None references in journal?! |
|
819 |
and ContentType.objects.get_for_model(reference) == user_ct |
|
820 |
and reference.uuid not in modified_users_uuids |
|
821 |
and reference.uuid not in unknown_uuids |
|
822 |
): |
|
823 |
modified_users_uuids.append(reference.uuid) |
|
824 |
return (known_uuids, unknown_uuids, modified_users_uuids) |
|
803 | 825 | |
804 | 826 |
def get_users_from_uuids(self, known_uuids): |
805 | 827 |
User = get_user_model() |
... | ... | |
813 | 835 |
response = {'result': 0, 'errors': serializer.errors} |
814 | 836 |
return Response(response, status.HTTP_400_BAD_REQUEST) |
815 | 837 |
hooks.call_hooks('api_modify_serializer_after_validation', self, serializer) |
816 |
known_uuids, unknown_uuids = self.check_uuids(serializer.validated_data.get('known_uuids', [])) |
|
817 |
full_known_users = serializer.validated_data.get('full_known_users', None) |
|
838 |
known_uuids = serializer.validated_data.get('known_uuids', []) |
|
839 |
timestamp = serializer.validated_data.get('timestamp', None) |
|
840 |
known_uuids, unknown_uuids, modified_users_uuids = self.check_uuids(known_uuids, timestamp) |
|
818 | 841 |
data = { |
819 | 842 |
'result': 1, |
820 | 843 |
'unknown_uuids': unknown_uuids, |
844 |
'modified_users_uuids': modified_users_uuids, |
|
821 | 845 |
} |
846 |
full_known_users = serializer.validated_data.get('full_known_users', None) |
|
822 | 847 |
if full_known_users: |
823 | 848 |
if len(known_uuids) > 1000: |
824 | 849 |
known_uuids = known_uuids[:1000] |
tests/test_api.py | ||
---|---|---|
1183 | 1183 |
}.issubset(set(user_dict.keys())) |
1184 | 1184 | |
1185 | 1185 | |
1186 |
def test_user_synchronization_timestamp(app, admin): |
|
1187 |
headers = basic_authorization_header(admin) |
|
1188 |
url = reverse('a2-api-users-synchronization') |
|
1189 |
now = datetime.datetime.now() |
|
1190 | ||
1191 |
ou = get_default_ou() |
|
1192 |
User = get_user_model() |
|
1193 |
users = [] |
|
1194 | ||
1195 |
for i in range(6): |
|
1196 |
users.append( |
|
1197 |
User.objects.create( |
|
1198 |
first_name='john%s' % i, |
|
1199 |
last_name='doe', |
|
1200 |
username='user%s' % i, |
|
1201 |
email='user%s' % i, |
|
1202 |
ou=ou, |
|
1203 |
) |
|
1204 |
) |
|
1205 | ||
1206 |
for i, event_name in enumerate( |
|
1207 |
[ |
|
1208 |
'manager.user.creation', |
|
1209 |
'manager.user.profile.edit', |
|
1210 |
'manager.user.activation', |
|
1211 |
'manager.user.deactivation', |
|
1212 |
'manager.user.password.change.force', |
|
1213 |
'manager.user.password.change.unforce', |
|
1214 |
] |
|
1215 |
): |
|
1216 |
event_type = EventType.objects.get_for_name(event_name) |
|
1217 |
Event.objects.create( |
|
1218 |
type=event_type, |
|
1219 |
timestamp=now - datetime.timedelta(days=i, hours=1), |
|
1220 |
references=[users[i]], |
|
1221 |
) |
|
1222 | ||
1223 |
content = { |
|
1224 |
'known_uuids': [user.uuid for user in users], |
|
1225 |
'timestamp': (now - datetime.timedelta(days=3)).isoformat(), |
|
1226 |
} |
|
1227 | ||
1228 |
response = app.post(url, params=content, headers=headers) |
|
1229 | ||
1230 |
for user in users[:3]: |
|
1231 |
assert user.uuid in response.json['modified_users_uuids'] |
|
1232 |
assert user.uuid not in response.json['unknown_uuids'] |
|
1233 |
for user in users[3:]: |
|
1234 |
assert user.uuid not in response.json['modified_users_uuids'] |
|
1235 |
assert user.uuid not in response.json['unknown_uuids'] |
|
1236 | ||
1237 |
for user in users[:3]: |
|
1238 |
user.delete() |
|
1239 | ||
1240 |
content['timestamp'] = (now - datetime.timedelta(days=7)).isoformat() |
|
1241 | ||
1242 |
response = app.post(url, params=content, headers=headers) |
|
1243 | ||
1244 |
for user in users[:3]: |
|
1245 |
assert user.uuid not in response.json['modified_users_uuids'] |
|
1246 |
assert user.uuid in response.json['unknown_uuids'] |
|
1247 |
for user in users[3:]: |
|
1248 |
assert user.uuid in response.json['modified_users_uuids'] |
|
1249 |
assert user.uuid not in response.json['unknown_uuids'] |
|
1250 | ||
1251 |
for user in users[3:]: |
|
1252 |
user.delete() |
|
1253 | ||
1254 |
response = app.post(url, params=content, headers=headers) |
|
1255 | ||
1256 |
assert not response.json['modified_users_uuids'] |
|
1257 |
for user in users: |
|
1258 |
assert user.uuid in response.json['unknown_uuids'] |
|
1259 | ||
1260 |
for user in users[:3]: |
|
1261 |
content['known_uuids'].remove(user.uuid) |
|
1262 | ||
1263 |
response = app.post(url, params=content, headers=headers) |
|
1264 | ||
1265 |
assert not response.json['modified_users_uuids'] |
|
1266 |
assert len(response.json['unknown_uuids']) == 3 |
|
1267 |
for user in users[3:]: |
|
1268 |
assert user.uuid in response.json['unknown_uuids'] |
|
1269 | ||
1270 | ||
1186 | 1271 |
def test_api_drf_authentication_class(app, admin, user_ou1, oidc_client): |
1187 | 1272 |
url = '/api/users/%s/' % user_ou1.uuid |
1188 | 1273 |
# test invalid client |
1189 |
- |