Add settings for client web-based logon flow.
Review Request #13045 — Created May 19, 2023 and submitted — Latest diff uploaded
This adds some settings to the Authentication settings page for the
upcoming client web-based logon flow. There is a new section for
general API Authentication settings, with a field for setting the
automatic expiration of client API tokens.
- Ran unit tests.
- Manually tested the settings page putting different expirations,
saving the form, playing with other settings.
Diff Revision 1
This is not the most recent revision of the diff. The latest diff is revision 3. See what's changed.
orig
1
2
3
Commits
reviewboard/admin/forms/auth_settings.py | |||
---|---|---|---|
Revision c60e413642a49097370017f1ecf8f849b60a754b | New Change | ||
1 |
"""Administration form for authentication settings."""
|
1 |
"""Administration form for authentication settings."""
|
2 | 2 | ||
3 |
import logging |
3 |
import logging |
4 |
from typing import Iterator, Tuple |
||
4 | 5 | ||
5 |
from django import forms |
6 |
from django import forms |
6 |
from django.utils.translation import gettext_lazy as _ |
7 |
from django.utils.translation import gettext_lazy as _ |
8 |
from djblets.forms.widgets import AmountSelectorWidget |
||
7 |
from djblets.siteconfig.forms import SiteSettingsForm |
9 |
from djblets.siteconfig.forms import SiteSettingsForm |
8 | 10 | ||
9 |
from reviewboard.accounts.forms.auth import LegacyAuthModuleSettingsForm |
11 |
from reviewboard.accounts.forms.auth import LegacyAuthModuleSettingsForm |
10 |
from reviewboard.admin.siteconfig import load_site_config |
12 |
from reviewboard.admin.siteconfig import load_site_config |
11 | 13 | ||
27 lines | |||
class AuthenticationSettingsForm(SiteSettingsForm):
|
|||
39 |
required=True, |
41 |
required=True, |
40 |
widget=forms.Select(attrs={ |
42 |
widget=forms.Select(attrs={ |
41 |
'data-subform-group': 'auth-backend', |
43 |
'data-subform-group': 'auth-backend', |
42 |
}))
|
44 |
}))
|
43 | 45 | ||
46 |
client_web_login_enabled = forms.BooleanField( |
||
47 |
label=_('Enable web-based logon for clients.'), |
||
48 |
help_text=_('Allow users to authenticate clients (e.g. RBTools) to ' |
||
49 |
'Review Board by logging in via a browser.'), |
||
50 |
required=False) |
||
51 | |||
44 |
def __init__(self, siteconfig, *args, **kwargs): |
52 |
def __init__(self, siteconfig, *args, **kwargs): |
45 |
"""Initialize the settings form. |
53 |
"""Initialize the settings form. |
46 | 54 | ||
47 |
This will load the list of available authentication backends and
|
55 |
This will load the list of available authentication backends and
|
48 |
their settings forms, allowing the browser to show the appropriate
|
56 |
their settings forms, allowing the browser to show the appropriate
|
88 lines | |||
def __init__(self, siteconfig, *args, **kwargs):
|
|||
137 | 145 | ||
138 |
form = backend.settings_form(siteconfig, *args, **kwargs) |
146 |
form = backend.settings_form(siteconfig, *args, **kwargs) |
139 |
form.load() |
147 |
form.load() |
140 |
self.sso_backend_forms[backend_id] = form |
148 |
self.sso_backend_forms[backend_id] = form |
141 | 149 | ||
150 |
# Settings for web-based client logon.
|
||
151 |
client_web_login_form = ClientWebBasedLoginSettingsForm(siteconfig, |
||
152 |
*args, |
||
153 |
**kwargs) |
||
154 |
client_web_login_form.load() |
||
155 |
self.client_web_login_forms = { |
||
156 |
'client_web_login': client_web_login_form, |
||
157 |
}
|
||
158 | |||
142 |
self.load() |
159 |
self.load() |
143 | 160 | ||
144 |
def load(self): |
161 |
def load(self): |
145 |
"""Load settings from the form. |
162 |
"""Load settings from the form. |
146 | 163 | ||
147 |
This will populate initial fields based on the site configuration.
|
164 |
This will populate initial fields based on the site configuration.
|
148 |
"""
|
165 |
"""
|
149 |
super(AuthenticationSettingsForm, self).load() |
166 |
super(AuthenticationSettingsForm, self).load() |
150 | 167 | ||
151 |
self.fields['auth_anonymous_access'].initial = \ |
168 |
self.fields['auth_anonymous_access'].initial = \ |
152 |
not self.siteconfig.get('auth_require_sitewide_login') |
169 |
not self.siteconfig.get('auth_require_sitewide_login') |
170 |
self.fields['client_web_login_enabled'].initial = \ |
||
171 |
self.siteconfig.get('client_web_login') |
||
153 | 172 | ||
154 |
def save(self): |
173 |
def save(self): |
155 |
"""Save the form. |
174 |
"""Save the form. |
156 | 175 | ||
157 |
This will write the new configuration to the database. It will then
|
176 |
This will write the new configuration to the database. It will then
|
158 |
force a site configuration reload.
|
177 |
force a site configuration reload.
|
159 |
"""
|
178 |
"""
|
160 |
self.siteconfig.set('auth_require_sitewide_login', |
179 |
self.siteconfig.set('auth_require_sitewide_login', |
161 |
not self.cleaned_data['auth_anonymous_access']) |
180 |
not self.cleaned_data['auth_anonymous_access']) |
181 |
self.siteconfig.set('client_web_login', |
||
182 |
self.cleaned_data['client_web_login_enabled']) |
||
162 | 183 | ||
163 |
auth_backend = self.cleaned_data['auth_backend'] |
184 |
auth_backend = self.cleaned_data['auth_backend'] |
164 | 185 | ||
165 |
if auth_backend in self.auth_backend_forms: |
186 |
if auth_backend in self.auth_backend_forms: |
166 |
self.auth_backend_forms[auth_backend].save() |
187 |
self.auth_backend_forms[auth_backend].save() |
167 | 188 | ||
168 2 |
for form, enable_field_id in self._iter_sso_backend_forms(): |
189 |
for form, enable_field_id in self._iter_sso_backend_forms(): |
169 |
if self[enable_field_id].data: |
190 |
if self[enable_field_id].data: |
170 |
form.save() |
191 |
form.save() |
171 | 192 | ||
193 |
for form, enable_field_id in self._iter_client_web_login_forms(): |
||
194 |
if self[enable_field_id].data: |
||
195 |
form.save() |
||
196 | |||
172 |
super(AuthenticationSettingsForm, self).save() |
197 |
super(AuthenticationSettingsForm, self).save() |
173 | 198 | ||
174 |
# Reload any important changes into the Django settings.
|
199 |
# Reload any important changes into the Django settings.
|
175 |
load_site_config() |
200 |
load_site_config() |
176 | 201 | ||
20 lines | |||
def is_valid(self):
|
|||
197 |
for form, enable_field_id in self._iter_sso_backend_forms(): |
222 |
for form, enable_field_id in self._iter_sso_backend_forms(): |
198 |
if (self.cleaned_data[enable_field_id] and |
223 |
if (self.cleaned_data[enable_field_id] and |
199 |
not form.is_valid()): |
224 |
not form.is_valid()): |
200 |
return False |
225 |
return False |
201 | 226 | ||
227 |
for form, enable_field_id in self._iter_client_web_login_forms(): |
||
228 |
if (self.cleaned_data[enable_field_id] and |
||
229 |
not form.is_valid()): |
||
230 |
return False |
||
231 | |||
202 |
return True |
232 |
return True |
203 | 233 | ||
204 |
def full_clean(self): |
234 |
def full_clean(self): |
205 |
"""Clean and validate all form fields. |
235 |
"""Clean and validate all form fields. |
206 | 236 | ||
18 lines | |||
def full_clean(self):
|
|||
225 | 255 | ||
226 |
for form, enable_field_id in self._iter_sso_backend_forms(): |
256 |
for form, enable_field_id in self._iter_sso_backend_forms(): |
227 |
if (self[enable_field_id].data or |
257 |
if (self[enable_field_id].data or |
228 |
self.fields[enable_field_id].initial): |
258 |
self.fields[enable_field_id].initial): |
229 |
form.full_clean() |
259 |
form.full_clean() |
260 | |||
261 |
for form, enable_field_id in self._iter_client_web_login_forms(): |
||
1 | 262 |
if (self[enable_field_id].data): |
|
263 |
form.full_clean() |
||
230 |
else: |
264 |
else: |
231 |
for form in self.auth_backend_forms.values(): |
265 |
for form in self.auth_backend_forms.values(): |
232 |
form.full_clean() |
266 |
form.full_clean() |
233 | 267 | ||
234 |
for form in self.sso_backend_forms.values(): |
268 |
for form in self.sso_backend_forms.values(): |
235 |
form.full_clean() |
269 |
form.full_clean() |
236 | 270 | ||
271 |
for form in self.client_web_login_forms.values(): |
||
272 |
form.full_clean() |
||
273 | |||
237 |
def _iter_sso_backend_forms(self): |
274 |
def _iter_sso_backend_forms(self): |
238 |
"""Yield the SSO backend forms. |
275 |
"""Yield the SSO backend forms. |
239 | 276 | ||
240 |
Yields:
|
277 |
Yields:
|
241 |
tuple:
|
278 |
tuple:
|
242 |
A 2-tuple of the SSO backend form and the name of the form field
|
279 |
A 2-tuple of the SSO backend form and the name of the form field
|
243 |
to enable that backend.
|
280 |
to enable that backend.
|
244 |
"""
|
281 |
"""
|
245 |
for sso_backend_id, form in self.sso_backend_forms.items(): |
282 |
for sso_backend_id, form in self.sso_backend_forms.items(): |
246 |
enable_field_id = '%s_enabled' % sso_backend_id |
283 |
enable_field_id = '%s_enabled' % sso_backend_id |
247 | 284 | ||
248 |
yield form, enable_field_id |
285 |
yield form, enable_field_id |
249 | 286 | ||
287 |
def _iter_client_web_login_forms(self) -> Iterator[Tuple[forms.Form, str]]: |
||
288 |
"""Yield the client web-based login settings forms. |
||
289 | |||
290 |
This only yields one form.
|
||
291 | |||
292 |
Yields:
|
||
293 |
tuple:
|
||
294 |
A 2-tuple of:
|
||
295 | |||
296 |
Tuple:
|
||
297 |
0 (django.forms.Form):
|
||
298 |
The client web-based login settings form.
|
||
299 | |||
300 |
1 (str):
|
||
301 |
The name of the form field to enable the client web-based
|
||
302 |
login flow.
|
||
303 |
"""
|
||
304 |
for field_id, form in self.client_web_login_forms.items(): |
||
305 |
enable_field_id = '%s_enabled' % field_id |
||
306 | |||
307 |
yield form, enable_field_id |
||
308 | |||
250 |
class Meta: |
309 |
class Meta: |
251 |
title = _('Authentication Settings') |
310 |
title = _('Authentication Settings') |
252 |
save_blacklist = ('auth_anonymous_access',) |
311 |
save_blacklist = ('auth_anonymous_access',) |
253 | 312 | ||
254 |
subforms = ( |
313 |
subforms = ( |
4 lines | |||
class Meta:
|
|||
259 |
{
|
318 |
{
|
260 |
'subforms_attr': 'sso_backend_forms', |
319 |
'subforms_attr': 'sso_backend_forms', |
261 |
'controller_field': None, |
320 |
'controller_field': None, |
262 |
'enable_checkbox': True, |
321 |
'enable_checkbox': True, |
263 |
},
|
322 |
},
|
323 |
{
|
||
324 |
'subforms_attr': 'client_web_login_forms', |
||
325 |
'controller_field': None, |
||
326 |
'enable_checkbox': True, |
||
327 |
},
|
||
264 |
)
|
328 |
)
|
265 | 329 | ||
266 |
fieldsets = ( |
330 |
fieldsets = ( |
267 |
{
|
331 |
{
|
268 |
'classes': ('wide',), |
332 |
'classes': ('wide',), |
269 |
'fields': ['auth_anonymous_access', 'auth_backend'], |
333 |
'fields': ['auth_anonymous_access', |
334 |
'auth_backend', |
||
335 |
'client_web_login_enabled'], |
||
270 |
},
|
336 |
},
|
271 |
)
|
337 |
)
|
338 | |||
339 | |||
340 |
class ClientWebBasedLoginSettingsForm(SiteSettingsForm): |
||
341 |
"""A form for configuring the settings for client web-based login. |
||
342 | |||
343 |
Version Added:
|
||
344 |
5.0.5
|
||
345 |
"""
|
||
346 | |||
347 |
api_token_expiration = forms.IntegerField( |
||
348 |
label=_('Client API token expiration'), |
||
349 |
help_text=_('API tokens are automatically created to authenticate ' |
||
350 |
'clients during web-based login. This sets their '
|
||
351 |
'expiration.'), |
||
352 |
required=False, |
||
1 | 353 |
widget=AmountSelectorWidget(unit_choices=[ |
|
354 |
(1, _('days')), |
||
355 |
(7, _('weeks')), |
||
356 |
(30, _('months')), |
||
357 |
(365, _('years')), |
||
358 |
(None, _('Never')), |
||
359 |
], number_attrs={ |
||
360 |
'min': 0, |
||
361 |
}))
|
||
362 | |||
363 |
def load(self) -> None: |
||
364 |
"""Load settings from the form. |
||
365 | |||
366 |
This will populate initial fields based on the site configuration.
|
||
367 |
"""
|
||
368 |
super().load() |
||
369 | |||
370 |
self.fields['api_token_expiration'].initial = \ |
||
371 |
self.siteconfig.get('client_web_login_token_expiration') |
||
372 | |||
373 |
def save( |
||
374 |
self, |
||
375 |
*args, |
||
376 |
**kwargs |
||
377 |
) -> None: |
||
378 |
"""Save the form. |
||
379 | |||
380 |
This will write the new configuration to the database. It will then
|
||
381 |
force a site configuration reload.
|
||
382 | |||
383 |
Args:
|
||
384 |
*args (tuple):
|
||
385 |
Positional arguments to pass to the parent method.
|
||
386 | |||
387 |
**kwargs (dict):
|
||
388 |
Keyword arguments to pass to the parent method.
|
||
389 |
"""
|
||
390 |
self.siteconfig.set('client_web_login_token_expiration', |
||
391 |
self.cleaned_data['api_token_expiration']) |
||
392 | |||
393 |
super().save(*args, **kwargs) |
||
394 | |||
1 | 395 |
# Reload any important changes into the Django settings.
|
|
396 |
load_site_config() |
||
397 | |||
398 |
class Meta: |
||
399 |
title = _('Client Web-based Logon Settings') |
reviewboard/admin/tests/test_web_client_logon_settings_form.py |
---|