management: cancel sync deletion on abnormally high ratio (#62849)

This commit is contained in:
Paul Marillonnet 2022-03-23 16:21:58 +01:00
parent 4b001bd77e
commit 6dc0d61030
6 changed files with 106 additions and 19 deletions

View File

@ -17,14 +17,13 @@
from __future__ import print_function
import datetime
import requests
from django.conf import settings
from django.core.management.base import BaseCommand
from django.core.exceptions import MultipleObjectsReturned
from authentic2.utils.template import Template
from authentic2_auth_oidc.models import OIDCProvider, OIDCAccount
from authentic2_auth_oidc.models import OIDCAccount, OIDCProvider
from django.conf import settings
from django.core.exceptions import MultipleObjectsReturned
from django.core.management.base import BaseCommand
class Command(BaseCommand):
@ -51,14 +50,19 @@ class Command(BaseCommand):
resp.raise_for_status()
unknown_uuids.extend(resp.json().get('unknown_uuids'))
for account in OIDCAccount.objects.filter(sub__in=unknown_uuids):
if verbose:
print('disabling', account.user.email, account.user.ou)
account.user.email = account.user.email + '.%s.invalid' % (
datetime.datetime.now().strftime('%Y-%m-%dT%H-%M-%S')
)
account.user.save()
OIDCAccount.objects.filter(sub__in=unknown_uuids).delete()
deletion_ratio = len(unknown_uuids) / OIDCAccount.objects.filter(provider=cut_users).count()
if deletion_ratio > 0.05: # higher than 5%, something definitely went wrong
print(f'deletion ratio is abnormally high ({deletion_ratio}), aborting unkwown users deletion')
else:
for account in OIDCAccount.objects.filter(sub__in=unknown_uuids):
if verbose:
print('disabling', account.user.email, account.user.ou)
account.user.email = account.user.email + '.%s.invalid' % (
datetime.datetime.now().strftime('%Y-%m-%dT%H-%M-%S')
)
account.user.save()
OIDCAccount.objects.filter(sub__in=unknown_uuids).delete()
# update recently modified users
url = settings.CUT_API_BASE_URL + 'users/?modified__gt=%s' % (

0
tests/__init__.py Normal file
View File

View File

@ -10,6 +10,8 @@ try:
except ImportError:
import pathlib2 as pathlib
from authentic2.a2_rbac.utils import get_default_ou
from authentic2_auth_oidc.models import OIDCProvider
from django.contrib.auth import get_user_model
from django.core.management import call_command
from django_rbac.utils import get_ou_model
@ -64,6 +66,11 @@ def user(db):
return user
@pytest.fixture
def oidc_provider(db):
return OIDCProvider.objects.create(name='CUT', slug='cut', ou=get_default_ou())
@pytest.fixture
def hooks(settings):
if hasattr(settings, 'A2_HOOKS'):
@ -79,7 +86,7 @@ def hooks(settings):
@pytest.fixture
def admin(db):
user = User(username='admin', email='admin@example.net', is_superuser=True, is_staff=True)
user.ou = OU.objects.get(slug='territoire')
user.ou = get_default_ou()
user.set_password('admin')
user.save()
return user

View File

@ -14,6 +14,9 @@ if 'postgres' in DATABASES['default']['ENGINE']:
if key in os.environ:
DATABASES['default'][key[2:]] = os.environ[key]
CUT_API_BASE_URL = 'https://cut.base.provider/'
CUT_API_CREDENTIALS = ('abc', 'xyz')
LANGUAGE_CODE = 'en'
A2_FC_CLIENT_ID = ''
A2_FC_CLIENT_SECRET = ''

View File

@ -1,7 +1,79 @@
import random
import uuid
import httmock
import pytest
import requests
from authentic2.a2_rbac.utils import get_default_ou
from authentic2.utils import crypto
from authentic2_auth_oidc.models import OIDCAccount
from django.contrib.auth import get_user_model
from django.core.management import call_command
def test_dummy(db, app):
assert 1
def test_another_dummy(db, app):
assert 1
@pytest.mark.parametrize('deletion_number_and_validity', [(2, True), (5, True), (10, False)])
def test_user_synchronization_deletion_threshold(
db, app, admin, settings, capsys, oidc_provider, deletion_number_and_validity
):
User = get_user_model()
deletion_number = deletion_number_and_validity[0]
deletion_valid = deletion_number_and_validity[1]
for i in range(100):
user = User.objects.create(
first_name='John%s' % i,
last_name='Doe%s' % i,
username='john.doe.%s' % i,
email='john.doe.%s@ad.dre.ss',
ou=get_default_ou(),
)
identifier = uuid.UUID(user.uuid).bytes
sector_identifier = 'cut'
cipher_args = [
settings.SECRET_KEY.encode('utf-8'),
identifier,
sector_identifier,
]
sub = crypto.aes_base64url_deterministic_encrypt(*cipher_args).decode('utf-8')
OIDCAccount.objects.create(user=user, provider=oidc_provider, sub=sub)
def synchronization_post_deletion_response(url, request):
headers = {'content-type': 'application/json'}
content = {
'unknown_uuids': [
account.sub for account in random.sample(list(OIDCAccount.objects.all()), deletion_number)
]
}
return httmock.response(status_code=200, headers=headers, content=content, request=request)
def synchronization_get_modified_response(url, request):
headers = {'content-type': 'application/json'}
content = {'results': [user.to_json() for user in random.sample(list(User.objects.all()), 20)]}
return httmock.response(status_code=200, headers=headers, content=content, request=request)
with httmock.HTTMock(
httmock.urlmatch(
netloc=r'cut\.base\.provider',
path=r'^/users/synchronization/$',
method='POST',
)(synchronization_post_deletion_response)
):
with httmock.HTTMock(
httmock.urlmatch(
netloc=r'cut\.base\.provider',
path=r'^/users/*',
method='GET',
)(synchronization_get_modified_response)
):
call_command('sync-cut', '--delta', '300', '-v1')
out, err = capsys.readouterr()
assert not err
if deletion_valid:
# existing users check
assert out.count('disabling') == deletion_number
assert OIDCAccount.objects.count() == 100 - deletion_number
else:
assert 'deletion ratio is abnormally high' in out
assert OIDCAccount.objects.count() == 100
# users update
assert 'got 20 users' in out # fixme: further testing in dedicated unit test

View File

@ -49,6 +49,7 @@ deps =
pytest-random
django-webtest
pyquery
httmock
commands =
py3: ./getlasso3.sh