Add settings for client web-based logon flow.

Review Request #13045 — Created May 19, 2023 and submitted — Latest diff uploaded

Information

Review Board
release-5.0.x

Reviewers

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

First Last Summary ID Author
Support web-based login for clients.
0540962a83a1b9e1f652c60a13a4cdc22ea5e46c Michelle Aubin
Add settings for client web-based logon.
283fd7d68173d9a5d14e13e117aee8e3d094b180 Michelle Aubin
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
Loading...