Я реализовал для нашего серверного приложения (SP) django возможность входа в систему через SAML, как IDP, используя Keycloak. Он отлично работает, но я хочу написать тесты, чтобы убедиться, что вся логика выполняется правильно. Для этого я хочу сгенерировать почтовый запрос с SAML в качестве тела и имитировать (unittest.mock.patch) настоящий запрос. Но я застрял.
Вот мое представление django, которое принимает запросы на получение и публикацию, когда я пытаюсь войти через SAML:
class SamlLoginView(View):
@staticmethod
def prepare_django_request(request):
if 'HTTP_X_FORWARDED_FOR' in request.META:
server_port = 443
else:
server_port = request.META.get('SERVER_PORT')
result = {
'https': 'on' if request.is_secure() else 'off',
'http_host': request.META['HTTP_HOST'],
'script_name': request.META['PATH_INFO'],
'server_port': server_port,
'get_data': request.GET.copy(),
'post_data': request.POST.copy(),
}
return result
@never_cache
def get(self, *args, **kwargs):
req = SamlLoginView.prepare_django_request(self.request)
auth = OneLogin_Saml2_Auth(req, settings.SAML_IDP_SETTINGS)
return_url = self.request.GET.get('next') or settings.LOGIN_REDIRECT_URL
return HttpResponseRedirect(auth.login(return_to=return_url))
@never_cache
def post(self, *args, **kwargs):
req = SamlLoginView.prepare_django_request(self.request)
print(req['post_data']['SAMLResponse'])
auth = OneLogin_Saml2_Auth(req, settings.SAML_IDP_SETTINGS)
auth.process_response()
errors = auth.get_errors()
if not errors:
if auth.is_authenticated():
logger.info("Login", extra={'action': 'login',
'userid': auth.get_nameid()})
user = authenticate(request=self.request,
saml_authentication=auth)
login(self.request, user)
return HttpResponseRedirect("/")
else:
raise PermissionDenied()
else:
return HttpResponseBadRequest("Error when processing SAML Response: %s" % (', '.join(errors)))
В своих тестах я хотел напрямую вызывать метод post, в котором внутри будет самл:
class TestSamlLogin(TestCase):
def test_saml_auth(self, prepare):
client = APIClient()
url = reverse_lazy("miri_auth:samllogin")
saml_resp='<xml with saml response>'
resp = client.post(url, data=saml_resp)
но, очевидно, это показывает, что request.POST пуст.
Затем я решил сделать макет для функции prepare_django_request
и вручную вставить saml:
def mocked_prepare_request(request):
post_query_dict = QueryDict(mutable=True)
post_data = {
'SAMLResponse': saml_xml,
'RelayState': '/accounts/profile/'
}
post_query_dict.update(post_data)
result = {
'https': 'on',
'http_host': '<http-host>',
'script_name': '/api/auth/samllogin/',
'server_port': '443',
'get_data': {},
'post_data': post_query_dict,
}
return result
class TestSamlLogin(TestCase):
@patch('miri_auth.views.SamlLoginView.prepare_django_request', side_effect=mocked_prepare_request)
def test_saml_auth(self, prepare):
client = APIClient()
url = reverse_lazy("miri_auth:samllogin")
saml_resp='<xml with saml response>'
resp = client.post(url, data=saml_resp)
и в зависимости от того, как я передаю saml_xml
, он выдает разные ошибки, если я определяю его как строку:
with open(os.path.join(TEST_FILES_PATH, 'saml.xml')) as f:
saml_xml = " ".join([x.strip() for x in f])
он возвращает: lxml.etree.XMLSyntaxError: Start tag expected, '<' not found, line 1, column 1
, хотя я проверил вывод из saml_xml
в валидаторе xml, и он говорит, что xml действителен. Когда я пытаюсь разобрать файл в xml заранее, я получаю другую ошибку позже, библиотеки, с которыми я пытался разобрать:
import xml.etree.ElementTree as ET
from xml.dom import minidom
from lxml import etree
tree = etree.parse(os.path.join(TEST_FILES_PATH, 'saml.xml'))
он возвращает: TypeError: argument should be a bytes-like object or ASCII string, not '_ElementTree'
Отладка этих ошибок не привела меня к какому-либо решению.
Если у кого-то есть мысли, как это можно реализовать (Mocking response with SAML), или где я допустил ошибку, буду рад услышать.
Заранее спасибо