4
4
# --- BEGIN_HEADER ---
5
5
#
6
6
# reqpwresetaction - handle account password reset requests and send email to user
7
- # Copyright (C) 2003-2023 The MiG Project lead by Brian Vinter
7
+ # Copyright (C) 2003-2024 The MiG Project lead by Brian Vinter
8
8
#
9
9
# This file is part of MiG.
10
10
#
30
30
from __future__ import absolute_import
31
31
32
32
import os
33
- import time
34
33
import tempfile
34
+ import time
35
35
36
36
from mig .shared import returnvalues
37
37
from mig .shared .base import canonical_user_with_peers , generate_https_urls , \
38
38
fill_distinguished_name , cert_field_map , auth_type_description , \
39
39
mask_creds , is_gdp_user
40
40
from mig .shared .defaults import keyword_auto , RESET_TOKEN_TTL
41
41
from mig .shared .functional import validate_input , REJECT_UNSET
42
+ from mig .shared .griddaemons .https import default_max_user_hits , \
43
+ default_user_abuse_hits , default_proto_abuse_hits , hit_rate_limit , \
44
+ expire_rate_limit , validate_auth_attempt
42
45
from mig .shared .handlers import safe_handler , get_csrf_limit
46
+ from mig .shared .html import themed_styles , themed_scripts
43
47
from mig .shared .init import initialize_main_variables , find_entry
44
48
from mig .shared .notification import send_email
45
49
from mig .shared .pwcrypto import generate_reset_token
@@ -61,7 +65,22 @@ def main(client_id, user_arguments_dict):
61
65
"""Main function used by front end"""
62
66
63
67
(configuration , logger , output_objects , op_name ) = \
64
- initialize_main_variables (client_id , op_header = False , op_menu = False )
68
+ initialize_main_variables (client_id , op_header = False , op_title = False ,
69
+ op_menu = False )
70
+ # IMPORTANT: no title in init above so we MUST call it immediately here
71
+ # or basic styling will break on e.g. the check user result.
72
+ styles = themed_styles (configuration )
73
+ scripts = themed_scripts (configuration , logged_in = False )
74
+ title_entry = {'object_type' : 'title' ,
75
+ 'text' : '%s reset account password request' %
76
+ configuration .short_title ,
77
+ 'skipmenu' : True , 'style' : styles , 'script' : scripts }
78
+ output_objects .append (title_entry )
79
+ output_objects .append ({'object_type' : 'header' , 'text' :
80
+ '%s reset account password request' %
81
+ configuration .short_title
82
+ })
83
+
65
84
defaults = signature (configuration )[1 ]
66
85
(validate_status , accepted ) = validate_input (user_arguments_dict ,
67
86
defaults , output_objects ,
@@ -71,15 +90,20 @@ def main(client_id, user_arguments_dict):
71
90
logger .warning ('%s invalid input: %s' % (op_name , accepted ))
72
91
return (accepted , returnvalues .CLIENT_ERROR )
73
92
74
- title_entry = find_entry (output_objects , 'title' )
75
- title_entry ['text' ] = '%s reset account password request' % \
76
- configuration .short_title
77
- title_entry ['skipmenu' ] = True
78
- output_objects .append ({'object_type' : 'header' , 'text' :
79
- '%s reset account password request' %
80
- configuration .short_title
81
- })
82
-
93
+ # Seconds to delay next reset attempt after hitting rate limit
94
+ delay_retry = 900
95
+ scripts ['init' ] += '''
96
+ function update_reload_counter(cnt, delay) {
97
+ var remain = (delay - cnt);
98
+ $("#reload_counter").html(remain.toString());
99
+ if (cnt >= delay) {
100
+ /* Load previous page again without re-posting last attempt */
101
+ location = history.back();
102
+ } else {
103
+ setTimeout(function() { update_reload_counter(cnt+1, delay); }, 1000);
104
+ }
105
+ }
106
+ '''
83
107
smtp_server = configuration .smtp_server
84
108
85
109
cert_id = accepted ['cert_id' ][- 1 ].strip ()
@@ -118,8 +142,68 @@ def main(client_id, user_arguments_dict):
118
142
return (output_objects , returnvalues .CLIENT_ERROR )
119
143
120
144
mig_user = os .environ .get ('USER' , 'mig' )
145
+ client_addr = os .environ .get ('REMOTE_ADDR' , None )
146
+ tcp_port = int (os .environ .get ('REMOTE_PORT' , '0' ))
121
147
anon_migoid_url = configuration .migserver_https_sid_url
122
148
149
+ status = returnvalues .OK
150
+
151
+ # Rate limit password reset attempts for any cert_id from source addr
152
+ # to prevent excessive requests spamming users or overloading server.
153
+ # We do so no matter if cert_id matches a valid user to prevent disclosure.
154
+ # Rate limit does not affect reset for another ID from same address as
155
+ # that may be perfectly valid e.g. if behind a shared NAT-gateway.
156
+ proto = 'https'
157
+ disconnect , exceeded_rate_limit = False , False
158
+ # Clean up expired entries in persistent rate limit cache
159
+ expire_rate_limit (configuration , proto , fail_cache = delay_retry ,
160
+ expire_delay = delay_retry )
161
+ if hit_rate_limit (configuration , proto , client_addr , cert_id ,
162
+ max_user_hits = 1 ):
163
+ exceeded_rate_limit = True
164
+ # Update rate limits and write to auth log
165
+ (authorized , disconnect ) = validate_auth_attempt (
166
+ configuration ,
167
+ proto ,
168
+ op_name ,
169
+ cert_id ,
170
+ client_addr ,
171
+ tcp_port ,
172
+ secret = None ,
173
+ authtype_enabled = True ,
174
+ auth_reset = True ,
175
+ exceeded_rate_limit = exceeded_rate_limit ,
176
+ user_abuse_hits = default_user_abuse_hits ,
177
+ proto_abuse_hits = default_proto_abuse_hits ,
178
+ max_secret_hits = 1 ,
179
+ skip_notify = True ,
180
+ )
181
+
182
+ if exceeded_rate_limit or disconnect :
183
+ logger .warning ('Throttle %s for %s from %s - past rate limit' %
184
+ (op_name , cert_id , client_addr ))
185
+ # NOTE: we keep actual result in plain text for json extract
186
+ output_objects .append ({'object_type' : 'html_form' , 'text' : '''
187
+ <div class="vertical-spacer"></div>
188
+ <div class="error leftpad errortext">
189
+ ''' })
190
+ output_objects .append ({'object_type' : 'text' , 'text' : """
191
+ Invalid input or rate limit exceeded - please wait %d seconds before retrying.
192
+ """ % delay_retry
193
+ })
194
+ output_objects .append ({'object_type' : 'html_form' , 'text' : '''
195
+ </div>
196
+ <div class="vertical-spacer"></div>
197
+ <div class="info leftpad">
198
+ Origin will reload automatically in <span id="reload_counter">%d</span> seconds.
199
+ </div>
200
+ </div>
201
+ ''' % delay_retry })
202
+ scripts ['ready' ] += '''
203
+ setTimeout(function() { update_reload_counter(1, %d); }, 1000);
204
+ ''' % delay_retry
205
+ return (output_objects , status )
206
+
123
207
search_filter = default_search ()
124
208
if '/' in cert_id :
125
209
search_filter ['distinguished_name' ] = cert_id
@@ -215,4 +299,4 @@ def main(client_id, user_arguments_dict):
215
299
output_objects .append (
216
300
{'object_type' : 'link' , 'destination' : 'javascript:history.back();' ,
217
301
'class' : 'genericbutton' , 'text' : "Back" })
218
- return (output_objects , returnvalues . OK )
302
+ return (output_objects , status )
0 commit comments