saml: always retry user creation when detecting duplicates on sso (#75777)
gitea/wcs/pipeline/head This commit looks good Details

This commit is contained in:
Benjamin Dauvergne 2023-03-24 12:02:08 +01:00
parent e01c6b65cc
commit b000de2f9b
3 changed files with 90 additions and 0 deletions

View File

@ -1,4 +1,5 @@
import os
import threading
import uuid
import pytest
@ -9,6 +10,7 @@ from wcs.qommon import force_str
from wcs.qommon import storage as st
from wcs.qommon.afterjobs import AfterJob
from wcs.qommon.http_request import HTTPRequest
from wcs.sql import cleanup_connection
from .utilities import create_temporary_pub, get_app
@ -937,3 +939,34 @@ def test_provision_http_endpoint(pub):
get_app(pub).post_json(sign_url('/__provision__/?orig=coucou&sync=1', '1234'), notification)
assert AfterJob.count() == 0 # sync
assert pub.user_class.count() == 1
def test_provisionning_concurrency(pub):
concurrency = 10
user_data = {
'uuid': 'a' * 32,
'first_name': 'John',
'last_name': 'Doé',
'email': 'john.doe@example.net',
'zipcode': '13400',
'is_superuser': False,
'is_active': True,
'roles': [],
}
for i in range(3):
pub.user_class.wipe()
b = threading.Barrier(concurrency)
def thread_function(b):
b.wait()
CmdHoboNotify.create_or_update_user(pub, user_data)
cleanup_connection()
threads = [threading.Thread(target=thread_function, args=[b]) for i in range(concurrency)]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
assert pub.user_class.count() == 1

View File

@ -2,6 +2,7 @@ import datetime
import http.cookies
import os
import shutil
import threading
import urllib.parse
import uuid
@ -23,6 +24,7 @@ from wcs.qommon.http_request import HTTPRequest
from wcs.qommon.ident.idp import MethodAdminDirectory
from wcs.qommon.misc import get_lasso_server
from wcs.qommon.saml2 import Saml2Directory, SOAPException
from wcs.sql import cleanup_connection
from .test_fc_auth import get_session
from .test_hobo_notify import PROFILE
@ -742,3 +744,28 @@ def test_opened_session_backoffice_url(pub):
resp = app.get('/backoffice/studio/')
assert resp.status_int == 302
assert urllib.parse.parse_qs(urllib.parse.urlparse(resp.location).query).get('IsPassive')
def test_sso_provisionning_concurrency(pub):
concurrency = 10
directory = Saml2Directory()
login = mock.Mock()
login.identity.dump.return_value = ''
for i in range(3):
pub.user_class.wipe()
b = threading.Barrier(concurrency)
def thread_function(b):
b.wait()
directory.get_or_create_user_by_name_id(login, 'abcd')
cleanup_connection()
threads = [threading.Thread(target=thread_function, args=[b]) for i in range(concurrency)]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
assert pub.user_class.count() == 1

View File

@ -15,6 +15,7 @@
# along with this program; if not, see <http://www.gnu.org/licenses/>.
import os
import random
import time
import urllib.parse
from xml.sax.saxutils import escape
@ -554,6 +555,35 @@ class Saml2Directory(Directory):
if save:
user.store()
def get_or_create_user_by_name_id(self, login, ni, retry=0):
user_class = get_publisher().user_class
if retry > 3:
raise Exception('user lookup on sso failed after %s tries.' % retry)
if retry > 0:
time.sleep(random.random() * 2 * retry)
users = sorted(
user_class.get_users_with_name_identifier(ni), key=lambda u: (u.last_seen or 0, -int(u.id))
)
if users:
# if multiple users, use the more recently used or the younger
user = users[-1]
else:
user = get_publisher().user_class(ni)
user.name_identifiers = [ni]
if login.identity:
user.lasso_dump = login.identity.dump()
user.store()
others = user_class.get_users_with_name_identifier(ni)
# there is an user mapping to the same id with a younger id:
# try again.
if len(others) > 1:
user.remove_self()
return self.get_or_create_user_by_name_id(login, ni, retry=retry + 1)
return user
def lookup_user(self, session, login):
if not login.nameIdentifier or not login.nameIdentifier.content:
return None