|
43 | 43 | iso3166 = None |
44 | 44 |
|
45 | 45 | from mig.shared.accountstate import default_account_expire |
46 | | -from mig.shared.base import force_utf8, force_native_str_rec, canonical_user, \ |
| 46 | +from mig.shared.base import auth_type_description, canonical_user, \ |
47 | 47 | client_id_dir, distinguished_name_to_user, fill_distinguished_name, \ |
48 | | - fill_user, auth_type_description, mask_creds |
| 48 | + fill_user, force_utf8, force_native_str_rec, get_user_id, mask_creds |
49 | 49 | from mig.shared.defaults import peers_fields, peers_filename, \ |
50 | 50 | pending_peers_filename, keyword_auto, user_db_filename, \ |
51 | 51 | gdp_distinguished_field |
52 | 52 | from mig.shared.fileio import delete_file, make_temp_file |
53 | 53 | from mig.shared.notification import notify_user |
| 54 | +from mig.shared.pwcrypto import check_hash |
54 | 55 | # Expose some helper variables for functionality backends |
55 | 56 | from mig.shared.safeinput import name_extras, password_extras, \ |
56 | 57 | password_min_len, password_max_len, valid_password_chars, \ |
57 | 58 | valid_name_chars, dn_max_len, html_escape, validated_input, REJECT_UNSET |
58 | 59 | from mig.shared.serial import load, dump, dumps |
59 | 60 | from mig.shared.useradm import user_request_reject, user_account_notify, \ |
60 | | - default_search, search_users, create_user, load_user_dict |
61 | | -from mig.shared.userdb import default_db_path |
| 61 | + default_search, search_users, create_user |
| 62 | +from mig.shared.userdb import default_db_path, load_user_dict |
62 | 63 | from mig.shared.validstring import valid_email_addresses |
63 | 64 |
|
64 | 65 |
|
@@ -1062,8 +1063,9 @@ def reject_account_req(req_id, configuration, reject_reason, |
1062 | 1063 | user_copy=True, admin_copy=True, auth_type='oid'): |
1063 | 1064 | """Helper to reject a pending account request""" |
1064 | 1065 | _logger = configuration.logger |
1065 | | - _logger.info('reject account request %s with msg %s' % |
1066 | | - (req_id, reject_reason)) |
| 1066 | + # NOTE: we strip newlines to avoid multi-line log entries |
| 1067 | + _logger.info('reject account request %s with reason: %r' % |
| 1068 | + (req_id, reject_reason.strip('\n'))) |
1067 | 1069 | # NOTE: conf_path accepts configuration object |
1068 | 1070 | conf_path = configuration |
1069 | 1071 | db_path = default_db_path(configuration) |
@@ -1173,6 +1175,103 @@ def list_country_codes(configuration): |
1173 | 1175 | return country_list |
1174 | 1176 |
|
1175 | 1177 |
|
| 1178 | +def existing_user_collision(configuration, raw_request, client_id): |
| 1179 | + """Check if raw_request has collisions with existing users in user DB. |
| 1180 | + Mainly if email is already bound to another user with different full name, |
| 1181 | + organization, country or state. |
| 1182 | + """ |
| 1183 | + logger = configuration.logger |
| 1184 | + db_path = default_db_path(configuration) |
| 1185 | + search_filter = default_search() |
| 1186 | + search_filter['email'] = raw_request.get('email', 'UNSET') |
| 1187 | + (_, hits) = search_users(search_filter, configuration, db_path) |
| 1188 | + collisions = [user_id for (user_id, _) in hits if user_id != client_id] |
| 1189 | + if collisions: |
| 1190 | + logger.warning('one or more ID collisions in request from %r: %s' % \ |
| 1191 | + (client_id, ', '.join(collisions))) |
| 1192 | + return True |
| 1193 | + else: |
| 1194 | + return False |
| 1195 | + |
| 1196 | +def early_validation_checks(configuration, raw_request, service, username, |
| 1197 | + password): |
| 1198 | + """Early validation checks including e.g. password check when change is not |
| 1199 | + authorized. Useful to allow janitor to request invalid requests with |
| 1200 | + sufficient delay to render various user enumeration, email and password |
| 1201 | + guessing scenarios infeasible""" |
| 1202 | + logger = configuration.logger |
| 1203 | + illegal_pw_change = """invalid password in renewal request. |
| 1204 | +Please use your existing password when renewing to prove account ownership. You |
| 1205 | +can use the 'Forgot password' link on the login page to securely reset it first |
| 1206 | +if needed""" |
| 1207 | + renewal_blocked = """account status blocks renewal request. |
| 1208 | +Please contact support if you haven't been informed why this might be and think |
| 1209 | +your account access should be renewed""" |
| 1210 | + id_collision = """invalid ID in account creation request. |
| 1211 | +An existing user has overlapping but not identical ID fields. You must reuse |
| 1212 | +your exact existing ID to renew account access. Please contact support if you |
| 1213 | +want corrections or affiliation changes in your site registration""" |
| 1214 | + invalid_full_name = """invalid full name in account creation request. |
| 1215 | +You must provide your full name to get account access due to various legal |
| 1216 | +requirements e.g. in relation to account abuse. Please contact support if you |
| 1217 | +have questions in that regard""" |
| 1218 | + missing_peers_info = """missing peer info in account creation request. |
| 1219 | +You must point to one or more persons allowed to invite peers with their full |
| 1220 | +name and email address they have used for site registration. You can ask them |
| 1221 | +to invite you instead if that is easier""" |
| 1222 | + # Lazy init |
| 1223 | + raw_request['invalid'] = raw_request.get('invalid', []) |
| 1224 | + |
| 1225 | + client_id = get_user_id(configuration, raw_request) |
| 1226 | + db_path = default_db_path(configuration) |
| 1227 | + user_dict = load_user_dict(configuration, client_id, db_path) |
| 1228 | + if user_dict: |
| 1229 | + # Renewal or password change |
| 1230 | + authorized = raw_request.get('authorized', None) |
| 1231 | + reset_token = raw_request.get('reset_token', None) |
| 1232 | + account_status = raw_request.get('status', 'active') |
| 1233 | + if not authorized and not reset_token: |
| 1234 | + hashed = user_dict.get('password_hash', None) |
| 1235 | + if not check_hash(configuration, service, username, password, |
| 1236 | + hashed): |
| 1237 | + logger.warning('illegal password change in request from %r' % \ |
| 1238 | + client_id) |
| 1239 | + raw_request['invalid'].append(illegal_pw_change) |
| 1240 | + elif account_status not in ('temporal', 'active', 'inactive'): |
| 1241 | + logger.warning('existing account for %r is %s and not renewable' \ |
| 1242 | + % (client_id, account_status)) |
| 1243 | + raw_request['invalid'].append(renewal_blocked) |
| 1244 | + else: |
| 1245 | + logger.debug('account renewal from %r looks alright' % client_id) |
| 1246 | + elif existing_user_collision(configuration, raw_request, client_id): |
| 1247 | + # TODO: drop and rely solely on live check in janitor to avoid races? |
| 1248 | + logger.warning('ID collision in request from %r' % client_id) |
| 1249 | + raw_request['invalid'].append(id_collision) |
| 1250 | + else: |
| 1251 | + # New user account |
| 1252 | + peers_full_name = raw_request.get('peers_full_name', None) |
| 1253 | + peers_email = raw_request.get('peers_email', None) |
| 1254 | + full_name = raw_request.get('full_name', 'UNSET') |
| 1255 | + if configuration.site_enable_peers and \ |
| 1256 | + ('email' in configuration.site_peers_explicit_fields and \ |
| 1257 | + not peers_email or \ |
| 1258 | + 'full_name' in configuration.site_peers_explicit_fields and \ |
| 1259 | + not peers_full_name): |
| 1260 | + logger.warning('missing peers field in request from %r: %r %r' % \ |
| 1261 | + (client_id, peers_full_name, peers_email)) |
| 1262 | + raw_request['invalid'].append(missing_peers_info) |
| 1263 | + elif len(full_name.split(' ')) < 2: |
| 1264 | + # TODO: prevent this at the source instead - sign up and peers |
| 1265 | + logger.warning('invalid single word full name in request from %r' \ |
| 1266 | + % client_id) |
| 1267 | + raw_request['invalid'].append(invalid_full_name) |
| 1268 | + # TODO: check that specified peers have accounts and can act as peers |
| 1269 | + # TODO: check for other obvious signup errors ? |
| 1270 | + else: |
| 1271 | + logger.debug('account request from %r looks alright' % client_id) |
| 1272 | + return raw_request |
| 1273 | + |
| 1274 | + |
1176 | 1275 | def prefilter_potential_peers(peers_list, configuration): |
1177 | 1276 | """Return entries from peers_list that fit local prefilter policy""" |
1178 | 1277 | potential_peers = [] |
|
0 commit comments