management: cancel sync deletion on abnormally high ratio (#62849)
This commit is contained in:
parent
4b001bd77e
commit
6dc0d61030
|
@ -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' % (
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = ''
|
||||
|
|
|
@ -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
|
||||
|
|
Reference in New Issue