Skip to content

Commit 372a174

Browse files
authored
[AAP-50320] Add missing api schema for /ui_auth endpoint (#782)
[AAP-50320] Add missing api schema for /ui_auth endpoint
1 parent 51a9675 commit 372a174

File tree

4 files changed

+267
-2
lines changed

4 files changed

+267
-2
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
from .authenticator import AuthenticatorSerializer # noqa: 401
22
from .authenticator_map import AuthenticatorMapSerializer # noqa: 401
3+
from .ui_auth import PasswordAuthenticatorSerializer, SSOAuthenticatorSerializer, UIAuthResponseSerializer # noqa: 401
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from rest_framework import serializers
2+
3+
4+
class PasswordAuthenticatorSerializer(serializers.Serializer):
5+
"""Serializer for password authenticator items in UI auth response."""
6+
7+
name = serializers.CharField(read_only=True)
8+
9+
10+
class SSOAuthenticatorSerializer(serializers.Serializer):
11+
"""Serializer for SSO authenticator items in UI auth response."""
12+
13+
name = serializers.CharField(read_only=True)
14+
login_url = serializers.URLField(read_only=True)
15+
type = serializers.CharField(read_only=True)
16+
17+
18+
class UIAuthResponseSerializer(serializers.Serializer):
19+
"""Serializer for UI authentication configuration response."""
20+
21+
passwords = PasswordAuthenticatorSerializer(many=True, read_only=True)
22+
ssos = SSOAuthenticatorSerializer(many=True, read_only=True)
23+
show_login_form = serializers.BooleanField(read_only=True)
24+
login_redirect_override = serializers.CharField(allow_blank=True, read_only=True)
25+
custom_login_info = serializers.CharField(allow_blank=True, read_only=True)
26+
custom_logo = serializers.CharField(allow_blank=True, read_only=True)
27+
managed_cloud_install = serializers.BooleanField(read_only=True)

ansible_base/authentication/views/ui_auth.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import logging
22

3+
from django.conf import settings
34
from django.utils.translation import gettext_lazy as _
45
from rest_framework.response import Response
56
from rest_framework.serializers import ValidationError
67

78
from ansible_base.authentication.models import Authenticator
9+
from ansible_base.authentication.serializers import UIAuthResponseSerializer
810
from ansible_base.lib.utils.settings import get_setting, is_aoc_instance
911
from ansible_base.lib.utils.validation import validate_image_data, validate_url
1012
from ansible_base.lib.utils.views.django_app_api import AnsibleBaseDjangoAppApiView
@@ -15,12 +17,27 @@
1517
class UIAuth(AnsibleBaseDjangoAppApiView):
1618
authentication_classes = []
1719
permission_classes = []
20+
serializer_class = UIAuthResponseSerializer
1821

19-
def get(self, request, format=None):
22+
def _get(self):
2023
response = generate_ui_auth_data()
21-
2224
return Response(response)
2325

26+
# Conditionally add openapi documentation
27+
if 'ansible_base.api_documentation' in settings.INSTALLED_APPS:
28+
from drf_spectacular.utils import extend_schema
29+
30+
@extend_schema(
31+
request=None, responses=UIAuthResponseSerializer, description="Get UI authentication configuration including available authenticators and settings."
32+
)
33+
def get(self, request, format=None):
34+
return self._get()
35+
36+
else:
37+
38+
def get(self):
39+
return self._get()
40+
2441

2542
def generate_ui_auth_data():
2643
authenticators = Authenticator.objects.filter(enabled=True)
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
from ansible_base.authentication.serializers.ui_auth import (
2+
PasswordAuthenticatorSerializer,
3+
SSOAuthenticatorSerializer,
4+
UIAuthResponseSerializer,
5+
)
6+
7+
8+
class TestPasswordAuthenticatorSerializer:
9+
def test_serializes_password_authenticator_data(self):
10+
"""Test PasswordAuthenticatorSerializer serializes data correctly"""
11+
data = {'name': 'password_auth'}
12+
serializer = PasswordAuthenticatorSerializer(data)
13+
assert serializer.data == data
14+
15+
def test_serializes_with_different_name(self):
16+
"""Test PasswordAuthenticatorSerializer with different name"""
17+
data = {'name': 'local_password'}
18+
serializer = PasswordAuthenticatorSerializer(data)
19+
assert serializer.data == data
20+
21+
22+
class TestSSOAuthenticatorSerializer:
23+
def test_serializes_sso_authenticator_data(self):
24+
"""Test SSOAuthenticatorSerializer serializes data correctly"""
25+
data = {'name': 'sso_auth', 'login_url': 'https://example.com/login', 'type': 'saml'}
26+
serializer = SSOAuthenticatorSerializer(data)
27+
assert serializer.data == data
28+
29+
def test_serializes_different_sso_types(self):
30+
"""Test SSOAuthenticatorSerializer with different SSO types"""
31+
saml_data = {'name': 'saml_auth', 'login_url': 'https://example.com/saml', 'type': 'saml'}
32+
oidc_data = {'name': 'oidc_auth', 'login_url': 'https://example.com/oidc', 'type': 'oidc'}
33+
34+
saml_serializer = SSOAuthenticatorSerializer(saml_data)
35+
oidc_serializer = SSOAuthenticatorSerializer(oidc_data)
36+
37+
assert saml_serializer.data == saml_data
38+
assert oidc_serializer.data == oidc_data
39+
40+
41+
class TestUIAuthResponseSerializer:
42+
def test_serializes_complete_auth_response(self):
43+
"""Test UIAuthResponseSerializer serializes complete data correctly"""
44+
data = {
45+
'passwords': [{'name': 'password_auth'}],
46+
'ssos': [{'name': 'sso_auth', 'login_url': 'https://example.com/login', 'type': 'saml'}],
47+
'show_login_form': True,
48+
'login_redirect_override': 'https://example.com/redirect',
49+
'custom_login_info': 'Please login with your credentials',
50+
'custom_logo': 'data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACwAAAAAAQABAAACAkQBADs=',
51+
'managed_cloud_install': False,
52+
}
53+
serializer = UIAuthResponseSerializer(data)
54+
assert serializer.data == data
55+
56+
def test_serializes_minimal_auth_response(self):
57+
"""Test UIAuthResponseSerializer serializes minimal data correctly"""
58+
data = {
59+
'passwords': [],
60+
'ssos': [],
61+
'show_login_form': False,
62+
'login_redirect_override': '',
63+
'custom_login_info': '',
64+
'custom_logo': '',
65+
'managed_cloud_install': False,
66+
}
67+
serializer = UIAuthResponseSerializer(data)
68+
assert serializer.data == data
69+
70+
def test_serializes_multiple_password_authenticators(self):
71+
"""Test UIAuthResponseSerializer with multiple password authenticators"""
72+
data = {
73+
'passwords': [{'name': 'password_auth_1'}, {'name': 'password_auth_2'}],
74+
'ssos': [],
75+
'show_login_form': True,
76+
'login_redirect_override': '',
77+
'custom_login_info': '',
78+
'custom_logo': '',
79+
'managed_cloud_install': False,
80+
}
81+
serializer = UIAuthResponseSerializer(data)
82+
result = serializer.data
83+
assert len(result['passwords']) == 2
84+
assert result['passwords'][0] == {'name': 'password_auth_1'}
85+
assert result['passwords'][1] == {'name': 'password_auth_2'}
86+
87+
def test_serializes_multiple_sso_authenticators(self):
88+
"""Test UIAuthResponseSerializer with multiple SSO authenticators"""
89+
data = {
90+
'passwords': [],
91+
'ssos': [
92+
{'name': 'saml_auth', 'login_url': 'https://example.com/saml', 'type': 'saml'},
93+
{'name': 'oidc_auth', 'login_url': 'https://example.com/oidc', 'type': 'oidc'},
94+
],
95+
'show_login_form': True,
96+
'login_redirect_override': '',
97+
'custom_login_info': '',
98+
'custom_logo': '',
99+
'managed_cloud_install': False,
100+
}
101+
serializer = UIAuthResponseSerializer(data)
102+
result = serializer.data
103+
assert len(result['ssos']) == 2
104+
assert result['ssos'][0] == {'name': 'saml_auth', 'login_url': 'https://example.com/saml', 'type': 'saml'}
105+
assert result['ssos'][1] == {'name': 'oidc_auth', 'login_url': 'https://example.com/oidc', 'type': 'oidc'}
106+
107+
def test_serializes_nested_authenticator_data(self):
108+
"""Test UIAuthResponseSerializer correctly serializes nested authenticator data"""
109+
data = {
110+
'passwords': [{'name': 'local_auth'}, {'name': 'ldap_auth'}],
111+
'ssos': [
112+
{'name': 'google_sso', 'login_url': 'https://accounts.google.com/oauth', 'type': 'oidc'},
113+
{'name': 'okta_sso', 'login_url': 'https://company.okta.com/saml', 'type': 'saml'},
114+
],
115+
'show_login_form': True,
116+
'login_redirect_override': '/dashboard',
117+
'custom_login_info': 'Use your company credentials',
118+
'custom_logo': 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
119+
'managed_cloud_install': True,
120+
}
121+
serializer = UIAuthResponseSerializer(data)
122+
result = serializer.data
123+
124+
# Verify top-level structure
125+
assert result == data
126+
127+
# Verify nested data is properly serialized
128+
assert len(result['passwords']) == 2
129+
assert len(result['ssos']) == 2
130+
assert result['show_login_form'] is True
131+
assert result['managed_cloud_install'] is True
132+
133+
def test_serializes_empty_authenticator_lists(self):
134+
"""Test UIAuthResponseSerializer with empty authenticator lists"""
135+
data = {
136+
'passwords': [],
137+
'ssos': [],
138+
'show_login_form': False,
139+
'login_redirect_override': '',
140+
'custom_login_info': '',
141+
'custom_logo': '',
142+
'managed_cloud_install': False,
143+
}
144+
serializer = UIAuthResponseSerializer(data)
145+
result = serializer.data
146+
assert result['passwords'] == []
147+
assert result['ssos'] == []
148+
assert result['show_login_form'] is False
149+
150+
def test_serializes_boolean_fields_correctly(self):
151+
"""Test UIAuthResponseSerializer handles boolean fields correctly"""
152+
true_data = {
153+
'passwords': [],
154+
'ssos': [],
155+
'show_login_form': True,
156+
'login_redirect_override': '',
157+
'custom_login_info': '',
158+
'custom_logo': '',
159+
'managed_cloud_install': True,
160+
}
161+
false_data = {
162+
'passwords': [],
163+
'ssos': [],
164+
'show_login_form': False,
165+
'login_redirect_override': '',
166+
'custom_login_info': '',
167+
'custom_logo': '',
168+
'managed_cloud_install': False,
169+
}
170+
171+
true_serializer = UIAuthResponseSerializer(true_data)
172+
false_serializer = UIAuthResponseSerializer(false_data)
173+
174+
assert true_serializer.data['show_login_form'] is True
175+
assert true_serializer.data['managed_cloud_install'] is True
176+
assert false_serializer.data['show_login_form'] is False
177+
assert false_serializer.data['managed_cloud_install'] is False
178+
179+
def test_serializes_string_fields_correctly(self):
180+
"""Test UIAuthResponseSerializer handles string fields correctly"""
181+
data = {
182+
'passwords': [],
183+
'ssos': [],
184+
'show_login_form': False,
185+
'login_redirect_override': 'https://example.com/custom-redirect',
186+
'custom_login_info': 'Welcome! Please sign in with your credentials.',
187+
'custom_logo': 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iMTAiPjxyZWN0IHdpZHRoPSIxMCIgaGVpZ2h0PSIxMCIgZmlsbD0iYmx1ZSIvPjwvc3ZnPg==',
188+
'managed_cloud_install': False,
189+
}
190+
serializer = UIAuthResponseSerializer(data)
191+
result = serializer.data
192+
193+
assert result['login_redirect_override'] == 'https://example.com/custom-redirect'
194+
assert result['custom_login_info'] == 'Welcome! Please sign in with your credentials.'
195+
assert result['custom_logo'].startswith('data:image/svg+xml;base64,')
196+
197+
def test_read_only_serializer_behavior(self):
198+
"""Test that the serializer behaves as read-only (for documentation purposes)"""
199+
# This test documents that the serializer is read-only
200+
# Input validation is not performed since all fields are read_only=True
201+
data = {
202+
'passwords': [],
203+
'ssos': [],
204+
'show_login_form': False,
205+
'login_redirect_override': '',
206+
'custom_login_info': '',
207+
'custom_logo': '',
208+
'managed_cloud_install': False,
209+
}
210+
211+
# When used as a response serializer (the intended use case)
212+
serializer = UIAuthResponseSerializer(data)
213+
assert serializer.data == data
214+
215+
# When used with invalid input data (which would normally fail validation)
216+
# it still works because read_only fields are ignored during validation
217+
invalid_input = {'invalid_field': 'invalid_value'}
218+
input_serializer = UIAuthResponseSerializer(data=invalid_input)
219+
assert input_serializer.is_valid() # Always valid since all fields are read_only
220+
assert input_serializer.validated_data == {} # Empty since fields are read_only

0 commit comments

Comments
 (0)