From 0ca228fa967902aba5949038a5afc807fad838bf Mon Sep 17 00:00:00 2001 From: Paul Marillonnet Date: Wed, 4 Mar 2020 14:31:36 +0100 Subject: [PATCH] python3: migrate authentic (#40407) --- hobo/agent/authentic2/provisionning.py | 25 +- hobo/multitenant/settings_loaders.py | 9 +- .../data_authentic_export_site.json | 478 +++++++++++------- tests_authentic/settings.py | 2 + tests_authentic/test_hobo_deploy.py | 41 +- tests_authentic/test_rest_authentication.py | 2 +- 6 files changed, 327 insertions(+), 230 deletions(-) diff --git a/hobo/agent/authentic2/provisionning.py b/hobo/agent/authentic2/provisionning.py index 16b2421..078dd4b 100644 --- a/hobo/agent/authentic2/provisionning.py +++ b/hobo/agent/authentic2/provisionning.py @@ -1,5 +1,5 @@ import json -from urlparse import urljoin +from django.utils.six.moves.urllib.parse import urljoin import threading import copy import logging @@ -8,6 +8,7 @@ from django.contrib.auth import get_user_model from django.db import connection from django.core.urlresolvers import reverse from django.conf import settings +from django.utils.encoding import force_text from django_rbac.utils import get_role_model, get_ou_model, get_role_parenting_model from hobo.agent.common import notify_agents @@ -25,9 +26,9 @@ logger = logging.getLogger(__name__) class Provisionning(threading.local): __slots__ = ['threads'] - threads = set() def __init__(self): + self.threads = set() self.stack = [] def start(self): @@ -100,7 +101,7 @@ class Provisionning(threading.local): def is_forbidden_technical_role(role): return role.slug.startswith('_') and not role.slug.startswith(tuple(allowed_technical_roles_prefixes)) - issuer = unicode(self.get_entity_id()) + issuer = force_text(self.get_entity_id()) if mode == 'provision': def user_to_json(ou, service, user, user_roles): @@ -151,8 +152,8 @@ class Provisionning(threading.local): for rp in RoleParenting.objects.filter(child__in=all_roles): parents.setdefault(rp.child.id, []).append(rp.parent.id) Through = Role.members.through - for u_id, r_id in Through.objects.filter(role__members__in=users).values_list('user_id', - 'role_id'): + qs = Through.objects.filter(role__members__in=users).values_list('user_id', 'role_id') + for u_id, r_id in qs: user_roles.setdefault(u_id, set()).add(roles[r_id]) for p_id in parents.get(r_id, []): user_roles[u_id].add(roles[p_id]) @@ -163,7 +164,7 @@ class Provisionning(threading.local): ous.setdefault(r.ou, set()).add(user) if roles_with_attributes: - for ou, users in ous.iteritems(): + for ou, users in ous.items(): for service, audience in self.get_audience(ou): for user in users: logger.info(u'provisionning user %s to %s', user, audience) @@ -178,12 +179,12 @@ class Provisionning(threading.local): } }) else: - for ou, users in ous.iteritems(): + for ou, users in ous.items(): audience = [a for service, a in self.get_audience(ou)] if not audience: continue - logger.info(u'provisionning users %s to %s', - u', '.join(map(unicode, users)), u', '.join(audience)) + logger.info(u'provisionning users %s to %s', u', '.join( + map(force_text, users)), u', '.join(audience)) notify_agents({ '@type': 'provision', 'issuer': issuer, @@ -197,8 +198,8 @@ class Provisionning(threading.local): elif users: audience = [audience for ou in OU.objects.all() for s, audience in self.get_audience(ou)] - logger.info(u'deprovisionning users %s from %s', u', '.join(map(unicode, users)), - u', '.join(audience)) + logger.info(u'deprovisionning users %s from %s', u', '.join( + map(force_text, users)), u', '.join(audience)) notify_agents({ '@type': 'deprovision', 'issuer': issuer, @@ -263,7 +264,7 @@ class Provisionning(threading.local): }) global_roles = set(ous.get(None, [])) - for ou, ou_roles in ous.iteritems(): + for ou, ou_roles in ous.items(): sent_roles = set(ou_roles) | global_roles helper(ou, sent_roles) diff --git a/hobo/multitenant/settings_loaders.py b/hobo/multitenant/settings_loaders.py index 941aa0d..f66e4c6 100644 --- a/hobo/multitenant/settings_loaders.py +++ b/hobo/multitenant/settings_loaders.py @@ -1,10 +1,9 @@ import os import json import hashlib -from importlib import import_module from django.conf import settings -from django.utils.encoding import smart_bytes +from django.utils.encoding import force_bytes from django.utils.http import urlencode from django.utils.six.moves.urllib import parse as urlparse @@ -277,14 +276,14 @@ class CookieNames(object): return 0 def update_settings(self, tenant_settings, tenant): - domain_hash = hashlib.md5(smart_bytes(tenant.domain_url)).hexdigest()[:6] + domain_hash = hashlib.md5(force_bytes(tenant.domain_url)).hexdigest()[:6] tenant_settings.CSRF_COOKIE_NAME = 'csrftoken-%s' % domain_hash tenant_settings.SESSION_COOKIE_NAME = 'sessionid-%s' % domain_hash # unique but common name for authentic opened session cookie name if getattr(tenant_settings, 'TEMPLATE_VARS', None): idp_url = tenant_settings.TEMPLATE_VARS.get('idp_url') if idp_url: - idp_hash = hashlib.md5(smart_bytes(idp_url)).hexdigest()[:6] + idp_hash = hashlib.md5(force_bytes(idp_url)).hexdigest()[:6] cookie_name = 'a2-opened-session-%s' % idp_hash tenant_settings.A2_OPENED_SESSION_COOKIE_NAME = cookie_name tenant_settings.MELLON_OPENED_SESSION_COOKIE_NAME = cookie_name @@ -308,7 +307,7 @@ class Authentic(FileBaseSettingsLoader): if not getattr(tenant_settings, 'A2_IDP_OIDC_JWKSET', None): from jwcrypto import jwk jwkkey = jwk.JWK.from_pem( - tenant_settings.A2_IDP_SAML2_SIGNATURE_PRIVATE_KEY) + force_bytes(tenant_settings.A2_IDP_SAML2_SIGNATURE_PRIVATE_KEY)) jwkset = jwk.JWKSet() jwkset['keys'].add(jwkkey) tenant_settings.A2_IDP_OIDC_JWKSET = json.loads(jwkset.export()) diff --git a/tests_authentic/data_authentic_export_site.json b/tests_authentic/data_authentic_export_site.json index edf9004..9b42034 100644 --- a/tests_authentic/data_authentic_export_site.json +++ b/tests_authentic/data_authentic_export_site.json @@ -43,166 +43,38 @@ "uuid": "18e7bf78dc9a432396a99f32060052ec" }, { - "attributes": [ - { - "kind": "string", - "name": "is_superuser", - "value": "true" - } - ], "description": "", "external_id": "", - "name": "Administrateur de Compte citoyen", - "ou": { - "name": "Collectivit\u00e9 par d\u00e9faut", - "slug": "default", - "uuid": "69b0a02cf58a4c71b1ae548f1375baff" - }, - "service": { - "ou": { - "name": "Collectivit\u00e9 par d\u00e9faut", - "slug": "default", - "uuid": "69b0a02cf58a4c71b1ae548f1375baff" - }, - "slug": "portal" - }, - "slug": "_a2-hobo-superuser", - "uuid": "84b3b1ba76e44bcdb4fd4437c448a981" - }, - { - "attributes": [ - { - "kind": "string", - "name": "is_superuser", - "value": "true" - } - ], - "description": "", - "external_id": "", - "name": "Administrateur de D\u00e9marches", - "ou": { - "name": "Collectivit\u00e9 par d\u00e9faut", - "slug": "default", - "uuid": "69b0a02cf58a4c71b1ae548f1375baff" - }, - "service": { - "ou": { - "name": "Collectivit\u00e9 par d\u00e9faut", - "slug": "default", - "uuid": "69b0a02cf58a4c71b1ae548f1375baff" - }, - "slug": "eservices" - }, - "slug": "_a2-hobo-superuser", - "uuid": "9054a61ccf684396b38189f1ca1ec087" - }, - { - "attributes": [ - { - "kind": "string", - "name": "is_superuser", - "value": "true" - } - ], - "description": "", - "external_id": "", - "name": "Administrateur de Hobo", - "ou": { - "name": "Collectivit\u00e9 par d\u00e9faut", - "slug": "default", - "uuid": "69b0a02cf58a4c71b1ae548f1375baff" - }, - "service": { - "ou": { - "name": "Collectivit\u00e9 par d\u00e9faut", - "slug": "default", - "uuid": "69b0a02cf58a4c71b1ae548f1375baff" - }, - "slug": "hobo" - }, - "slug": "_a2-hobo-superuser", - "uuid": "25f33158b7e2449b9a5b00dbc57bf416" - }, - { - "attributes": [ - { - "kind": "string", - "name": "is_superuser", - "value": "true" - } - ], - "description": "", - "external_id": "", - "name": "Administrateur de Passerelle", - "ou": { - "name": "Collectivit\u00e9 par d\u00e9faut", - "slug": "default", - "uuid": "69b0a02cf58a4c71b1ae548f1375baff" - }, - "service": { - "ou": { - "name": "Collectivit\u00e9 par d\u00e9faut", - "slug": "default", - "uuid": "69b0a02cf58a4c71b1ae548f1375baff" - }, - "slug": "passerelle" - }, - "slug": "_a2-hobo-superuser", - "uuid": "243f58712aa248e9b27aae669341c156" - }, - { - "attributes": [ - { - "kind": "string", - "name": "is_superuser", - "value": "true" - } - ], - "description": "", - "external_id": "", - "name": "Administrateur de Portail agent", - "ou": { - "name": "Collectivit\u00e9 par d\u00e9faut", - "slug": "default", - "uuid": "69b0a02cf58a4c71b1ae548f1375baff" - }, - "service": { - "ou": { - "name": "Collectivit\u00e9 par d\u00e9faut", - "slug": "default", - "uuid": "69b0a02cf58a4c71b1ae548f1375baff" - }, - "slug": "portal-agent" - }, - "slug": "_a2-hobo-superuser", - "uuid": "e6e22e5c0ca04ac0bf3b50d88eafe6d5" - }, - { - "description": "", - "external_id": "", - "name": "Administrateur", + "name": "Manager", "ou": null, "parents": [ { - "name": "Administrateur des entit\u00e9s", + "name": "Manager of organizational units", "ou": null, "service": null, - "slug": "_a2-administrateur-des-entites", - "uuid": "a1ff1b3da88f47cea91e344998dfdfbf" + "slug": "_a2-manager-of-organizational-units", + "uuid": "b0e5379e65e34494b3d1feb1ffdd5234" }, { - "name": "Administrateur des r\u00f4les", + "name": "Manager of roles", "ou": null, "service": null, - "slug": "_a2-administrateur-des-roles", - "uuid": "8dd625b74cff40aa8531d7d72616550e" + "slug": "_a2-manager-of-roles", + "uuid": "1b58e06308c5493f9571484139b13aa4" }, { - "name": "Administrateur des utilisateurs", + "name": "Manager of services", "ou": null, "service": null, - "slug": "_a2-administrateur-des-utilisateurs", - "uuid": "4ab5effedc404fb1bcba4d21ee89b719" + "slug": "_a2-manager-of-services", + "uuid": "72faf45646c44054a60c11404d526f9b" + }, + { + "name": "Manager of users", + "ou": null, + "service": null, + "slug": "_a2-manager-of-users", + "uuid": "47ef76b626c44d5eaf8d6de7fe8c4cdb" } ], "permissions": [ @@ -212,11 +84,11 @@ }, "ou": null, "target": { - "name": "Administrateur", + "name": "Manager", "ou": null, "service": null, "slug": "_a2-manager", - "uuid": "81a8708382bb4e8ea12ed0e172aa48b9" + "uuid": "f6a4d39b864a40b68d2e0068ece14e14" }, "target_ct": { "app_label": "a2_rbac", @@ -226,12 +98,12 @@ ], "service": null, "slug": "_a2-manager", - "uuid": "81a8708382bb4e8ea12ed0e172aa48b9" + "uuid": "f6a4d39b864a40b68d2e0068ece14e14" }, { "description": "", "external_id": "", - "name": "Administrateur des entit\u00e9s", + "name": "Manager of organizational units", "ou": null, "permissions": [ { @@ -261,30 +133,16 @@ "app_label": "contenttypes", "model": "contenttype" } - }, - { - "operation": { - "slug": "view" - }, - "ou": null, - "target": { - "app_label": "a2_rbac", - "model": "organizationalunit" - }, - "target_ct": { - "app_label": "contenttypes", - "model": "contenttype" - } } ], "service": null, - "slug": "_a2-administrateur-des-entites", - "uuid": "a1ff1b3da88f47cea91e344998dfdfbf" + "slug": "_a2-manager-of-organizational-units", + "uuid": "b0e5379e65e34494b3d1feb1ffdd5234" }, { "description": "", "external_id": "", - "name": "Administrateur des r\u00f4les", + "name": "Manager of roles", "ou": null, "permissions": [ { @@ -315,20 +173,6 @@ "model": "contenttype" } }, - { - "operation": { - "slug": "view" - }, - "ou": null, - "target": { - "app_label": "a2_rbac", - "model": "organizationalunit" - }, - "target_ct": { - "app_label": "contenttypes", - "model": "contenttype" - } - }, { "operation": { "slug": "view" @@ -345,13 +189,52 @@ } ], "service": null, - "slug": "_a2-administrateur-des-roles", - "uuid": "8dd625b74cff40aa8531d7d72616550e" + "slug": "_a2-manager-of-roles", + "uuid": "1b58e06308c5493f9571484139b13aa4" }, { "description": "", "external_id": "", - "name": "Administrateur des utilisateurs", + "name": "Manager of services", + "ou": null, + "permissions": [ + { + "operation": { + "slug": "admin" + }, + "ou": null, + "target": { + "app_label": "authentic2", + "model": "service" + }, + "target_ct": { + "app_label": "contenttypes", + "model": "contenttype" + } + }, + { + "operation": { + "slug": "search" + }, + "ou": null, + "target": { + "app_label": "a2_rbac", + "model": "organizationalunit" + }, + "target_ct": { + "app_label": "contenttypes", + "model": "contenttype" + } + } + ], + "service": null, + "slug": "_a2-manager-of-services", + "uuid": "72faf45646c44054a60c11404d526f9b" + }, + { + "description": "", + "external_id": "", + "name": "Manager of users", "ou": null, "permissions": [ { @@ -381,30 +264,77 @@ "app_label": "contenttypes", "model": "contenttype" } + } + ], + "service": null, + "slug": "_a2-manager-of-users", + "uuid": "47ef76b626c44d5eaf8d6de7fe8c4cdb" + }, + { + "description": "", + "external_id": "", + "name": "Managers of \"Collectivit\u00e9 par d\u00e9faut\"", + "ou": null, + "parents": [ + { + "name": "Roles - Collectivit\u00e9 par d\u00e9faut", + "ou": { + "name": "Collectivit\u00e9 par d\u00e9faut", + "slug": "default", + "uuid": "69b0a02cf58a4c71b1ae548f1375baff" + }, + "service": null, + "slug": "_a2-manager-of-roles-default", + "uuid": "aeedec8fb6b9499ba3818095fd60d626" }, + { + "name": "Services - Collectivit\u00e9 par d\u00e9faut", + "ou": { + "name": "Collectivit\u00e9 par d\u00e9faut", + "slug": "default", + "uuid": "69b0a02cf58a4c71b1ae548f1375baff" + }, + "service": null, + "slug": "_a2-manager-of-services-default", + "uuid": "3945773a889f4264ac8b0f1f36972d96" + }, + { + "name": "Users - Collectivit\u00e9 par d\u00e9faut", + "ou": { + "name": "Collectivit\u00e9 par d\u00e9faut", + "slug": "default", + "uuid": "69b0a02cf58a4c71b1ae548f1375baff" + }, + "service": null, + "slug": "_a2-manager-of-users-default", + "uuid": "bd16770736bb42528c780d6e8e38a0de" + } + ], + "permissions": [ { "operation": { "slug": "view" }, "ou": null, "target": { - "app_label": "a2_rbac", - "model": "organizationalunit" + "name": "Collectivit\u00e9 par d\u00e9faut", + "slug": "default", + "uuid": "69b0a02cf58a4c71b1ae548f1375baff" }, "target_ct": { - "app_label": "contenttypes", - "model": "contenttype" + "app_label": "a2_rbac", + "model": "organizationalunit" } } ], "service": null, - "slug": "_a2-administrateur-des-utilisateurs", - "uuid": "4ab5effedc404fb1bcba4d21ee89b719" + "slug": "_a2-managers-of-default", + "uuid": "ef5e7ae84bc648608eb10dab6128ac7e" }, { "description": "", "external_id": "", - "name": "Administrateur du r\u00f4le \u00ab\u00a0Debug eo\u00a0\u00bb", + "name": "Managers of role \"Debug eo\"", "ou": { "name": "Collectivit\u00e9 par d\u00e9faut", "slug": "default", @@ -442,7 +372,7 @@ }, "ou": null, "target": { - "name": "Administrateur du r\u00f4le \u00ab\u00a0Debug eo\u00a0\u00bb", + "name": "Managers of role \"Debug eo\"", "ou": { "name": "Collectivit\u00e9 par d\u00e9faut", "slug": "default", @@ -450,7 +380,7 @@ }, "service": null, "slug": "_a2-managers-of-role-debug-eo", - "uuid": "3049444b35874b3b9a8377ad2f10b8b6" + "uuid": "ec1bee9eb7c040ad841b03916b593da2" }, "target_ct": { "app_label": "a2_rbac", @@ -474,7 +404,169 @@ ], "service": null, "slug": "_a2-managers-of-role-debug-eo", - "uuid": "3049444b35874b3b9a8377ad2f10b8b6" + "uuid": "ec1bee9eb7c040ad841b03916b593da2" + }, + { + "description": "", + "external_id": "", + "name": "Roles - Collectivit\u00e9 par d\u00e9faut", + "ou": { + "name": "Collectivit\u00e9 par d\u00e9faut", + "slug": "default", + "uuid": "69b0a02cf58a4c71b1ae548f1375baff" + }, + "permissions": [ + { + "operation": { + "slug": "admin" + }, + "ou": { + "name": "Collectivit\u00e9 par d\u00e9faut", + "slug": "default", + "uuid": "69b0a02cf58a4c71b1ae548f1375baff" + }, + "target": { + "app_label": "a2_rbac", + "model": "role" + }, + "target_ct": { + "app_label": "contenttypes", + "model": "contenttype" + } + }, + { + "operation": { + "slug": "search" + }, + "ou": null, + "target": { + "name": "Collectivit\u00e9 par d\u00e9faut", + "slug": "default", + "uuid": "69b0a02cf58a4c71b1ae548f1375baff" + }, + "target_ct": { + "app_label": "a2_rbac", + "model": "organizationalunit" + } + }, + { + "operation": { + "slug": "view" + }, + "ou": { + "name": "Collectivit\u00e9 par d\u00e9faut", + "slug": "default", + "uuid": "69b0a02cf58a4c71b1ae548f1375baff" + }, + "target": { + "app_label": "custom_user", + "model": "user" + }, + "target_ct": { + "app_label": "contenttypes", + "model": "contenttype" + } + } + ], + "service": null, + "slug": "_a2-manager-of-roles-default", + "uuid": "aeedec8fb6b9499ba3818095fd60d626" + }, + { + "description": "", + "external_id": "", + "name": "Services - Collectivit\u00e9 par d\u00e9faut", + "ou": { + "name": "Collectivit\u00e9 par d\u00e9faut", + "slug": "default", + "uuid": "69b0a02cf58a4c71b1ae548f1375baff" + }, + "permissions": [ + { + "operation": { + "slug": "admin" + }, + "ou": { + "name": "Collectivit\u00e9 par d\u00e9faut", + "slug": "default", + "uuid": "69b0a02cf58a4c71b1ae548f1375baff" + }, + "target": { + "app_label": "authentic2", + "model": "service" + }, + "target_ct": { + "app_label": "contenttypes", + "model": "contenttype" + } + }, + { + "operation": { + "slug": "search" + }, + "ou": null, + "target": { + "name": "Collectivit\u00e9 par d\u00e9faut", + "slug": "default", + "uuid": "69b0a02cf58a4c71b1ae548f1375baff" + }, + "target_ct": { + "app_label": "a2_rbac", + "model": "organizationalunit" + } + } + ], + "service": null, + "slug": "_a2-manager-of-services-default", + "uuid": "3945773a889f4264ac8b0f1f36972d96" + }, + { + "description": "", + "external_id": "", + "name": "Users - Collectivit\u00e9 par d\u00e9faut", + "ou": { + "name": "Collectivit\u00e9 par d\u00e9faut", + "slug": "default", + "uuid": "69b0a02cf58a4c71b1ae548f1375baff" + }, + "permissions": [ + { + "operation": { + "slug": "admin" + }, + "ou": { + "name": "Collectivit\u00e9 par d\u00e9faut", + "slug": "default", + "uuid": "69b0a02cf58a4c71b1ae548f1375baff" + }, + "target": { + "app_label": "custom_user", + "model": "user" + }, + "target_ct": { + "app_label": "contenttypes", + "model": "contenttype" + } + }, + { + "operation": { + "slug": "search" + }, + "ou": null, + "target": { + "name": "Collectivit\u00e9 par d\u00e9faut", + "slug": "default", + "uuid": "69b0a02cf58a4c71b1ae548f1375baff" + }, + "target_ct": { + "app_label": "a2_rbac", + "model": "organizationalunit" + } + } + ], + "service": null, + "slug": "_a2-manager-of-users-default", + "uuid": "bd16770736bb42528c780d6e8e38a0de" } ] } \ No newline at end of file diff --git a/tests_authentic/settings.py b/tests_authentic/settings.py index 0ce9b67..c51e0d4 100644 --- a/tests_authentic/settings.py +++ b/tests_authentic/settings.py @@ -44,3 +44,5 @@ HOBO_ROLE_EXPORT = True SESSION_COOKIE_SECURE = False CSRF_COOKIE_SECURE = False + +LANGUAGE_CODE = 'en' diff --git a/tests_authentic/test_hobo_deploy.py b/tests_authentic/test_hobo_deploy.py index 9572da1..01e5589 100644 --- a/tests_authentic/test_hobo_deploy.py +++ b/tests_authentic/test_hobo_deploy.py @@ -308,7 +308,7 @@ def test_hobo_deploy(monkeypatch, tenant_base, mocker, skeleton_dir): ] } hobo_json_content = json.dumps(env) - hobo_json = tempfile.NamedTemporaryFile() + hobo_json = tempfile.NamedTemporaryFile(mode='w') hobo_json.write(hobo_json_content) hobo_json.flush() @@ -463,28 +463,31 @@ def test_hobo_deploy(monkeypatch, tenant_base, mocker, skeleton_dir): def test_import_template(db, tenant_base): - def with_uuid_removed(input): - if isinstance(input, dict): - for key in input.keys(): - if key == 'uuid': - input.pop('uuid') - return {k: with_uuid_removed(v) for k, v in input.iteritems()} - elif isinstance(input, list): - return [with_uuid_removed(e) for e in input] - else: - return input + def listify(value): + if isinstance(value, dict): + value = list((k, listify(v)) for k, v in value.items()) + value.sort() + if isinstance(value, list): + value = list(listify(x) for x in value) + value.sort() + return value - def with_lists_sorted(input): - if isinstance(input, dict): - return {k: with_lists_sorted(v) for k, v in input.iteritems()} - if isinstance(input, list): - return with_lists_sorted(input.sort()) - else: - return input + def sort_and_remove_uuid(value): + if isinstance(value, dict): + if 'uuid' in value: + value.pop('uuid') + value = {k: sort_and_remove_uuid(v) for k, v in value.items()} + if isinstance(value, list): + value = [sort_and_remove_uuid(elt) for elt in value] + value.sort(key=lambda elt: listify(elt)) + return value call_command('create_tenant', 'authentic.example.net') tenant = TenantMiddleware.get_tenant_by_hostname('authentic.example.net') connection.set_tenant(tenant) call_command('import_template', '--basepath=%s' % os.path.dirname(__file__), 'data_authentic_export_site') content = open('%s/data_authentic_export_site.json' % os.path.dirname(__file__)).read() - assert byteify(with_lists_sorted(with_uuid_removed(export_site()))) == byteify(with_lists_sorted(with_uuid_removed(json.loads(content)))) + + export_ref = sort_and_remove_uuid(export_site()) + file_ref = sort_and_remove_uuid(json.loads(content)) + assert export_ref == file_ref diff --git a/tests_authentic/test_rest_authentication.py b/tests_authentic/test_rest_authentication.py index b769d56..349247d 100644 --- a/tests_authentic/test_rest_authentication.py +++ b/tests_authentic/test_rest_authentication.py @@ -1,5 +1,5 @@ import pytest -import urllib +from django.utils.six.moves.urllib import parse as urllib from rest_framework.exceptions import AuthenticationFailed