/
auth.py
525 lines (426 loc) · 20.3 KB
/
auth.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
import re
import sre_constants
from django import forms
from django.contrib.auth.models import User
from django.contrib.auth.forms import \
AuthenticationForm as DjangoAuthenticationForm
from django.core.exceptions import ValidationError
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from djblets.auth.ratelimit import is_ratelimited
from djblets.siteconfig.forms import SiteSettingsForm
from reviewboard.admin.checks import get_can_enable_ldap
class ActiveDirectorySettingsForm(SiteSettingsForm):
"""A form for configuring the Active Directory authentication backend."""
auth_ad_domain_name = forms.CharField(
label=_("Domain name"),
help_text=_("Enter the domain name to use, (ie. example.com). This "
"will be used to query for LDAP servers and to bind to "
"the domain."),
required=True,
widget=forms.TextInput(attrs={'size': '40'}))
auth_ad_use_tls = forms.BooleanField(
label=_("Use TLS for authentication"),
required=False)
auth_ad_find_dc_from_dns = forms.BooleanField(
label=_("Find DC from DNS"),
help_text=_("Query DNS to find which domain controller to use"),
required=False)
auth_ad_domain_controller = forms.CharField(
label=_("Domain controller"),
help_text=_("If not using DNS to find the DC, specify the domain "
"controller(s) here "
"(eg. ctrl1.example.com ctrl2.example.com:389)"),
required=False,
widget=forms.TextInput(attrs={'size': '40'}))
auth_ad_ou_name = forms.CharField(
label=_("OU name"),
help_text=_("Optionally restrict users to specified OU."),
required=False,
widget=forms.TextInput(attrs={'size': '40'}))
auth_ad_group_name = forms.CharField(
label=_("Group name"),
help_text=_("Optionally restrict users to specified group."),
required=False,
widget=forms.TextInput(attrs={'size': '40'}))
auth_ad_search_root = forms.CharField(
label=_("Custom search root"),
help_text=_("Optionally specify a custom search root, overriding "
"the built-in computed search root. If set, \"OU name\" "
"is ignored."),
required=False,
widget=forms.TextInput(attrs={'size': '40'}))
auth_ad_recursion_depth = forms.IntegerField(
label=_("Recursion Depth"),
help_text=_('Depth to recurse when checking group membership. '
'0 to turn off, -1 for unlimited.'),
required=False,
widget=forms.TextInput(attrs={'size': '40'}))
def load(self):
"""Load the data for the form."""
can_enable_ldap, reason = get_can_enable_ldap()
if not can_enable_ldap:
self.disabled_fields['auth_ad_use_tls'] = True
self.disabled_fields['auth_ad_group_name'] = True
self.disabled_fields['auth_ad_recursion_depth'] = True
self.disabled_fields['auth_ad_ou_name'] = True
self.disabled_fields['auth_ad_search_root'] = True
self.disabled_fields['auth_ad_find_dc_from_dns'] = True
self.disabled_fields['auth_ad_domain_controller'] = True
self.disabled_reasons['auth_ad_domain_name'] = reason
super(ActiveDirectorySettingsForm, self).load()
class Meta:
title = _('Active Directory Authentication Settings')
fieldsets = (
(None, {
'fields': ('auth_ad_domain_name',
'auth_ad_use_tls',
'auth_ad_find_dc_from_dns',
'auth_ad_domain_controller'),
}),
(_('Access Control Settings'), {
'fields': ('auth_ad_ou_name',
'auth_ad_group_name'),
}),
(_('Advanced Settings'), {
'fields': ('auth_ad_search_root',
'auth_ad_recursion_depth'),
}),
)
class StandardAuthSettingsForm(SiteSettingsForm):
"""A form for configuring the builtin authentication backend."""
auth_enable_registration = forms.BooleanField(
label=_("Enable registration"),
help_text=_("Allow users to register new accounts."),
required=False)
auth_registration_show_captcha = forms.BooleanField(
label=_('Show a captcha for registration'),
required=False)
recaptcha_public_key = forms.CharField(
label=_('reCAPTCHA Public Key'),
required=False,
widget=forms.TextInput(attrs={'size': '60'}))
recaptcha_private_key = forms.CharField(
label=_('reCAPTCHA Private Key'),
required=False,
widget=forms.TextInput(attrs={'size': '60'}))
def __init__(self, *args, **kwargs):
super(StandardAuthSettingsForm, self).__init__(*args, **kwargs)
# This is done at initialization time instead of in the field
# definition because the format operation causes the lazy translation
# to be evaluated, which has to happen after the i18n infrastructure
# has been started.
self.fields['auth_registration_show_captcha'].help_text = mark_safe(
_('Displays a captcha using <a href="%(recaptcha_url)s">'
'reCAPTCHA</a> on the registration page. To enable this, you '
'will need to go <a href="%(register_url)s">here</A> to '
'register an account and type in your new keys below.')
% {
'recaptcha_url': 'http://www.google.com/recaptcha',
'register_url': 'https://www.google.com/recaptcha/admin'
'#createsite',
})
def clean_recaptcha_public_key(self):
"""Validate that the reCAPTCHA public key is specified if needed."""
key = self.cleaned_data['recaptcha_public_key'].strip()
if self.cleaned_data['auth_registration_show_captcha'] and not key:
raise ValidationError(_('This field is required.'))
return key
def clean_recaptcha_private_key(self):
"""Validate that the reCAPTCHA private key is specified if needed."""
key = self.cleaned_data['recaptcha_private_key'].strip()
if self.cleaned_data['auth_registration_show_captcha'] and not key:
raise ValidationError(_('This field is required.'))
return key
class Meta:
title = _('Registration Settings')
fieldsets = (
(None, {
'fields': ('auth_enable_registration',),
}),
(_('reCAPTCHA Settings'), {
'fields': ('auth_registration_show_captcha',
'recaptcha_public_key',
'recaptcha_private_key'),
}),
)
class HTTPBasicSettingsForm(SiteSettingsForm):
"""A form for configuring the HTTP Digest authentication backend."""
auth_digest_file_location = forms.CharField(
label=_(".htpasswd File location"),
help_text=_("Location of the .htpasswd file which "
"stores the usernames and passwords in digest format"),
widget=forms.TextInput(attrs={'size': '60'}))
auth_digest_realm = forms.CharField(
label=_("HTTP Digest Realm"),
help_text=_("Realm used for HTTP Digest authentication"),
widget=forms.TextInput(attrs={'size': '40'}))
class Meta:
title = _('HTTP Digest Authentication Settings')
class LDAPSettingsForm(SiteSettingsForm):
"""A form for configuring the LDAP authentication backend."""
# TODO: Invent a URIField and use it.
auth_ldap_uri = forms.CharField(
label=_("LDAP Server"),
help_text=_("The LDAP server to authenticate with. "
"For example: ldap://localhost:389"),
widget=forms.TextInput(attrs={'size': '40'}))
auth_ldap_base_dn = forms.CharField(
label=_("LDAP Base DN"),
help_text=_("The LDAP Base DN for performing LDAP searches. For "
"example: ou=users,dc=example,dc=com"),
required=True,
widget=forms.TextInput(attrs={'size': '40'}))
auth_ldap_uid = forms.CharField(
label=_("Username Attribute"),
help_text=_("The attribute in the LDAP server that stores a user's "
"login name."),
required=True)
auth_ldap_given_name_attribute = forms.CharField(
label=_("Given Name Attribute"),
initial="givenName",
help_text=_("The attribute in the LDAP server that stores the user's "
"given name."),
required=False)
auth_ldap_surname_attribute = forms.CharField(
label=_("Surname Attribute"),
initial="sn",
help_text=_("The attribute in the LDAP server that stores the user's "
"surname."),
required=False)
auth_ldap_full_name_attribute = forms.CharField(
label=_("Full Name Attribute"),
help_text=_("The attribute in the LDAP server that stores the user's "
"full name. This takes precedence over the "
'"Given Name Attribute" and "Surname Attribute."'),
required=False)
auth_ldap_email_domain = forms.CharField(
label=_("E-Mail Domain"),
help_text=_("The domain name appended to the username to construct "
"the user's e-mail address. This takes precedence over "
'"E-Mail LDAP Attribute."'),
required=False,
widget=forms.TextInput(attrs={'size': '40'}))
auth_ldap_email_attribute = forms.CharField(
label=_("E-Mail LDAP Attribute"),
help_text=_("The attribute in the LDAP server that stores the user's "
"e-mail address. For example: mail"),
required=False)
auth_ldap_tls = forms.BooleanField(
label=_("Use TLS for authentication"),
required=False)
auth_ldap_uid_mask = forms.CharField(
label=_("Custom LDAP User Search Filter"),
required=False,
widget=forms.TextInput(attrs={'size': '40'}))
auth_ldap_anon_bind_uid = forms.CharField(
label=_("Review Board LDAP Bind Account"),
help_text=_("The full distinguished name of a user account with "
"sufficient access to perform lookups of users and "
"groups in the LDAP server. If the LDAP server permits "
"such lookups via anonymous bind, you may leave this "
"field blank, but we recommend disallowing anonymous "
"binds for security."),
required=False,
widget=forms.TextInput(attrs={'size': '40'}))
auth_ldap_anon_bind_passwd = forms.CharField(
label=_("Review Board LDAP Bind Password"),
widget=forms.PasswordInput(attrs={'size': '30'}, render_value=True),
help_text=_("The password for the Review Board LDAP Bind Account."),
required=False)
def __init__(self, *args, **kwargs):
super(LDAPSettingsForm, self).__init__(*args, **kwargs)
# This is done at initialization time instead of in the field
# definition because the format operation causes the lazy translation
# to be evaluated, which has to happen after the i18n infrastructure
# has been started.
self.fields['auth_ldap_uid_mask'].help_text = mark_safe(
_('A custom LDAP search filter, corresponding to RFC 2254. If '
'left unset, this option is equivalent to <tt>('
'usernameattribute=%(varname)s)</tt>. Use <tt>'
'"%(varname)s"</tt> wherever the username would normally go. '
'Specify this value only if the default cannot locate all '
'users.')
% {
'varname': '%s',
})
def load(self):
"""Load the data for the form."""
can_enable_ldap, reason = get_can_enable_ldap()
if not can_enable_ldap:
self.disabled_fields['auth_ldap_uri'] = True
self.disabled_fields['auth_ldap_given_name_attribute'] = True
self.disabled_fields['auth_ldap_surname_attribute'] = True
self.disabled_fields['auth_ldap_full_name_attribute'] = True
self.disabled_fields['auth_ldap_email_domain'] = True
self.disabled_fields['auth_ldap_email_attribute'] = True
self.disabled_fields['auth_ldap_tls'] = True
self.disabled_fields['auth_ldap_base_dn'] = True
self.disabled_fields['auth_ldap_uid'] = True
self.disabled_fields['auth_ldap_uid_mask'] = True
self.disabled_fields['auth_ldap_anon_bind_uid'] = True
self.disabled_fields['auth_ldap_anon_bind_password'] = True
self.disabled_reasons['auth_ldap_uri'] = reason
super(LDAPSettingsForm, self).load()
class Meta:
title = _('LDAP Authentication Settings')
fieldsets = (
(None, {
'fields': ('auth_ldap_uri',
'auth_ldap_tls',
'auth_ldap_anon_bind_uid',
'auth_ldap_anon_bind_passwd',
'auth_ldap_base_dn'),
}),
(_('User Lookups'), {
'fields': ('auth_ldap_uid',
'auth_ldap_given_name_attribute',
'auth_ldap_surname_attribute',
'auth_ldap_full_name_attribute',
'auth_ldap_email_attribute',
'auth_ldap_email_domain',
'auth_ldap_uid_mask'),
}),
)
class LegacyAuthModuleSettingsForm(SiteSettingsForm):
"""A form for configuring old-style custom authentication backends.
Newer authentication backends are registered via the extensions framework,
but there used to be a method by which users just put in a list of python
module paths. This form allows that configuration to be edited.
"""
custom_backends = forms.CharField(
label=_("Backends"),
help_text=_('A comma-separated list of old-style custom auth '
'backends. These are represented as Python module paths.'),
widget=forms.TextInput(attrs={'size': '40'}))
def load(self):
"""Load the data for the form."""
self.fields['custom_backends'].initial = \
', '.join(self.siteconfig.get('auth_custom_backends'))
super(LegacyAuthModuleSettingsForm, self).load()
def save(self):
"""Save the form."""
self.siteconfig.set(
'auth_custom_backends',
re.split(r',\s*', self.cleaned_data['custom_backends']))
super(LegacyAuthModuleSettingsForm, self).save()
class Meta:
title = _('Legacy Authentication Module Settings')
save_blacklist = ('custom_backends',)
class NISSettingsForm(SiteSettingsForm):
"""A form for configuring the NIS authentication backend."""
auth_nis_email_domain = forms.CharField(
label=_("E-Mail Domain"),
widget=forms.TextInput(attrs={'size': '40'}))
class Meta:
title = _('NIS Authentication Settings')
class X509SettingsForm(SiteSettingsForm):
"""A form for configuring the X509 certificate authentication backend."""
auth_x509_username_field = forms.ChoiceField(
label=_("Username Field"),
choices=(
# Note: These names correspond to environment variables set by
# mod_ssl.
("SSL_CLIENT_S_DN", _("DN (Distinguished Name)")),
("SSL_CLIENT_S_DN_CN", _("CN (Common Name)")),
("SSL_CLIENT_S_DN_Email", _("E-mail address")),
# Allow the user to use a custom environment variable
("CUSTOM", _("Custom Field")),
),
help_text=_("The X.509 certificate field from which the Review Board "
"username will be extracted."),
required=True)
auth_x509_custom_username_field = forms.CharField(
label=_("Custom Username Field"),
help_text=_("The custom X.509 certificate field from which the "
"Review Board username will be extracted. "
"(only used if Username Field is \"Custom Field\""),
required=False,
widget=forms.TextInput(attrs={'size': '40'}))
auth_x509_username_regex = forms.CharField(
label=_("Username Regex"),
help_text=_("Optional regex used to convert the selected X.509 "
"certificate field to a usable Review Board username. For "
"example, if using the e-mail field to retrieve the "
"username, use this regex to get the username from an "
"e-mail address: '(\\s+)@yoursite.com'. There must be "
"only one group in the regex."),
required=False,
widget=forms.TextInput(attrs={'size': '40'}))
auth_x509_autocreate_users = forms.BooleanField(
label=_("Automatically create new user accounts."),
help_text=_("Enabling this option will cause new user accounts to be "
"automatically created when a new user with an X.509 "
"certificate accesses Review Board."),
required=False)
def clean_auth_x509_username_regex(self):
"""Validate that the specified regular expression is valid."""
regex = self.cleaned_data['auth_x509_username_regex']
try:
re.compile(regex)
except sre_constants.error as e:
raise ValidationError(e)
return regex
class Meta:
title = _('X.509 Client Certificate Authentication Settings')
class ReviewBoardAuthenticationFormMixin(object):
"""Mixin for enhancing authentication forms.
This extends Django's built-in AuthenticationForm implementation to allow
users to specify their e-mail address in place of their username. In
addition, it also tracks the number of failed login attempts for a given
time frame, and informs the user whether the maximum number of attempts
have been exceeded.
"""
username = forms.CharField(
label=_('Username'),
widget=forms.TextInput(attrs={'autofocus': 'autofocus'}))
def clean_username(self):
"""Validate the 'username' field.
In case the given text is not a user found on the system, attempt a
look-up using it as an e-mail address and change the user-entered text
so that login can succeed.
"""
username = self.cleaned_data.get('username')
if not User.objects.filter(username=username).exists():
try:
username = User.objects.get(email=username).username
except (User.DoesNotExist, User.MultipleObjectsReturned):
pass
return username
def clean(self):
"""Validate the authentication form.
In case authentication has failed for the given user, Djblets's rate
limiting feature will increment the number of failed login attempts
until the maximum number of attempts have been reached. The user
will have to wait until the rate limit time period is over before
trying again.
Returns:
dict:
The cleaned data for all fields in the form.
Raises:
django.core.exceptions.ValidationError:
The data in the form was not valid.
"""
request = self.request
# Check if the number of failed login attempts were already exceeded
# before authenticating.
if is_ratelimited(request, increment=False):
raise forms.ValidationError(
_('Maximum number of login attempts exceeded.'))
try:
self.cleaned_data = \
super(ReviewBoardAuthenticationFormMixin, self).clean()
except ValidationError:
# If authentication for a given user has failed (i.e.
# self.user_cache is None), increment the number of
# failed login attempts.
is_ratelimited(request, increment=True)
raise
return self.cleaned_data
class AuthenticationForm(ReviewBoardAuthenticationFormMixin,
DjangoAuthenticationForm):
"""Standard authentication form for Review Board.
This is the form used whenever a user is logging into Review Board. It
will allow e-mail addresses or usernames for authentication, and will
rate limit login attempts.
"""