Skip to content

Commit 0c77a37

Browse files
committed
implement parsing account request Comment field for peer email addresses in reqacceptpeer.py and add new -E AUTO flag to enable it. Added a few sanity checks to avoid users vouching for themselves and expired or suspended accounts being used as peers in createuser while at it
git-svn-id: svn+ssh://svn.code.sf.net/p/migrid/code/trunk@5092 b75ad72c-e7d7-11dd-a971-7dbc132099af
1 parent be8d6b0 commit 0c77a37

File tree

4 files changed

+107
-25
lines changed

4 files changed

+107
-25
lines changed

mig/server/reqacceptpeer.py

+40-10
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# --- BEGIN_HEADER ---
55
#
66
# reqacceptpeer - Forward account request from peer to existing user(s)
7-
# Copyright (C) 2003-2020 The MiG Project lead by Brian Vinter
7+
# Copyright (C) 2003-2021 The MiG Project lead by Brian Vinter
88
#
99
# This file is part of MiG.
1010
#
@@ -32,6 +32,7 @@
3232
or email from Distinguished Name field of employee user entry. If user
3333
configured additional messaging protocols they can also be used.
3434
"""
35+
3536
from __future__ import print_function
3637
from __future__ import absolute_import
3738

@@ -48,6 +49,7 @@
4849
from mig.shared.serial import load, dump
4950
from mig.shared.useradm import init_user_adm, search_users, default_search, \
5051
user_account_notify
52+
from mig.shared.validstring import valid_email_addresses
5153

5254

5355
def usage(name='reqacceptpeer.py'):
@@ -62,7 +64,8 @@ def usage(name='reqacceptpeer.py'):
6264
-c CONF_FILE Use CONF_FILE as server configuration
6365
-C Send a copy of notifications to configured site admins
6466
-d DB_PATH Use DB_PATH as user data base file path
65-
-e EMAIL Send instructions to custom email address
67+
-e EMAIL Send instructions to custom EMAIL address
68+
-E EMAIL Forward peer request to user(s) with EMAIL (AUTO to parse Comment)
6669
-h Show this help
6770
-I CERT_DN Forward peer request to user(s) with ID (distinguished name)
6871
-s PROTOCOL Send instructions to notification protocol from settings
@@ -92,8 +95,9 @@ def usage(name='reqacceptpeer.py'):
9295
# IMPORTANT: Default to nobody to avoid spam if called without -I CLIENT_ID
9396
search_filter['distinguished_name'] = ''
9497
peer_dict = {}
98+
regex_keys = []
9599
exit_code = 0
96-
opt_args = 'ac:Cd:e:hI:s:u:v'
100+
opt_args = 'ac:Cd:e:E:hI:s:u:v'
97101
try:
98102
(opts, args) = getopt.getopt(args, opt_args)
99103
except getopt.GetoptError as err:
@@ -114,6 +118,11 @@ def usage(name='reqacceptpeer.py'):
114118
elif opt == '-e':
115119
raw_targets['email'] = raw_targets.get('email', [])
116120
raw_targets['email'].append(val)
121+
elif opt == '-E':
122+
if val != keyword_auto:
123+
search_filter['email'] = val.lower()
124+
else:
125+
search_filter['email'] = keyword_auto
117126
elif opt == '-h':
118127
usage()
119128
sys.exit(0)
@@ -179,24 +188,42 @@ def usage(name='reqacceptpeer.py'):
179188
fill_distinguished_name(peer_dict)
180189
peer_id = peer_dict['distinguished_name']
181190

191+
if search_filter['email'] == keyword_auto:
192+
peer_emails = valid_email_addresses(
193+
configuration, peer_dict['comment'])
194+
if peer_emails[1:]:
195+
regex_keys.append('email')
196+
search_filter['email'] = '(' + '|'.join(peer_emails) + ')'
197+
elif peer_emails:
198+
search_filter['email'] = peer_emails[0]
199+
elif search_filter['distinguished_name']:
200+
search_filter['email'] = '*'
201+
else:
202+
search_filter['email'] = ''
203+
204+
# If email is provided or detected DN may be almost anything
205+
if search_filter['email'] and not search_filter['distinguished_name']:
206+
search_filter['distinguished_name'] = '*emailAddress=*'
207+
182208
if verbose:
183-
print('Handling peer %s request to %s' %
184-
(peer_id, search_filter['distinguished_name']))
209+
print('Handling peer %s request to users matching %s' %
210+
(peer_id, search_filter))
185211

186212
# Lookup users to request formal acceptance from
187-
(_, hits) = search_users(search_filter, conf_path, db_path, verbose)
213+
(_, hits) = search_users(search_filter, conf_path,
214+
db_path, verbose, regex_match=regex_keys)
188215
logger = configuration.logger
189216
gdp_prefix = "%s=" % gdp_distinguished_field
190217

191218
if len(hits) < 1:
192219
print(
193220
"Aborting attempt to request peer acceptance without target users")
194-
print(" ... did you forget or supply too rigid -I CLIENT_ID argument?")
221+
print(" ... did you forget or supply too rigid -E EMAIL or -I DN arg?")
195222
sys.exit(1)
196223
elif len(hits) > 3:
197224
print("Aborting attempt to request peer acceptance from %d users!" %
198225
len(hits))
199-
print(" ... did you supply too lax -I CLIENT_ID argument?")
226+
print(" ... did you supply too lax -E EMAIL or -I DN argument?")
200227
sys.exit(1)
201228
else:
202229
if verbose:
@@ -213,9 +240,12 @@ def usage(name='reqacceptpeer.py'):
213240
print("Skip GDP project account: %s" % user_id)
214241
continue
215242

243+
if peer_id == user_id:
244+
print("Skip same user account %s as own peer" % user_id)
245+
continue
246+
216247
if not peers_permit_allowed(configuration, user_dict):
217-
if verbose:
218-
print("Skip account %s without vouching permission" % user_id)
248+
print("Skip account %s without vouching permission" % user_id)
219249
continue
220250

221251
if not manage_pending_peers(configuration, user_id, "add",

mig/shared/base.py

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
#
2727

2828
"""Base helper functions"""
29+
2930
from __future__ import print_function
3031
from __future__ import absolute_import
3132

mig/shared/useradm.py

+32-13
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# --- BEGIN_HEADER ---
55
#
66
# useradm - user administration functions
7-
# Copyright (C) 2003-2020 The MiG Project lead by Brian Vinter
7+
# Copyright (C) 2003-2021 The MiG Project lead by Brian Vinter
88
#
99
# This file is part of MiG.
1010
#
@@ -26,6 +26,7 @@
2626
#
2727

2828
"""User administration functions"""
29+
2930
from __future__ import print_function
3031
from __future__ import absolute_import
3132

@@ -65,7 +66,7 @@
6566
from mig.shared.settings import update_settings, update_profile, update_widgets
6667
from mig.shared.sharelinks import load_share_links, update_share_link, \
6768
get_share_link, mode_chars_map
68-
from mig.shared.validstring import possible_user_id
69+
from mig.shared.validstring import possible_user_id, valid_email_addresses
6970
from mig.shared.vgrid import vgrid_add_owners, vgrid_remove_owners, \
7071
vgrid_add_members, vgrid_remove_members, in_vgrid_share, \
7172
vgrid_sharelinks, vgrid_add_sharelinks
@@ -226,8 +227,7 @@ def create_user(
226227
peer_email_list = []
227228
# extract email of vouchee from comment if possible
228229
comment = user.get('comment', '')
229-
# NOTE: address must end in letter(s) to avoid trailing period, etc.
230-
all_matches = re.findall(r'[\w\.-]+@[\w\.-]+[\w]+', comment)
230+
all_matches = valid_email_addresses(configuration, comment)
231231
for i in all_matches:
232232
peer_email = "%s" % i
233233
if not possible_user_id(configuration, peer_email):
@@ -257,26 +257,45 @@ def create_user(
257257
peer_notes = []
258258
if not hits:
259259
peer_notes.append("no match for peers")
260-
for (user_id, _) in hits:
261-
_logger.debug("check %s in peers for %s" % (client_id, user_id))
262-
accepted_peers = get_accepted_peers(configuration, user_id)
260+
for (sponsor_id, sponsor_dict) in hits:
261+
_logger.debug("check %s in peers for %s" % (client_id, sponsor_id))
262+
if client_id == sponsor_id:
263+
warn_msg = "users cannot vouch for themselves: %s for %s" % \
264+
(client_id, sponsor_id)
265+
_logger.warning(warn_msg)
266+
continue
267+
sponsor_expire = sponsor_dict.get('expire', -1)
268+
if sponsor_expire >= 0 and time.time() > sponsor_expire:
269+
warn_msg = "expire %s prevents %s as peer for %s" % \
270+
(sponsor_expire, sponsor_id, client_id)
271+
_logger.warning(warn_msg)
272+
peer_notes.append(warn_msg)
273+
continue
274+
sponsor_status = sponsor_dict.get('status', 'active')
275+
if sponsor_status not in ['active', 'temporal']:
276+
warn_msg = "status %s prevents %s as peer for %s" % \
277+
(sponsor_status, sponsor_id, client_id)
278+
_logger.warning(warn_msg)
279+
peer_notes.append(warn_msg)
280+
continue
281+
accepted_peers = get_accepted_peers(configuration, sponsor_id)
263282
peer_entry = accepted_peers.get(client_id, None)
264283
if not peer_entry:
265284
_logger.warning("could not validate %s as peer for %s" %
266-
(user_id, client_id))
285+
(sponsor_id, client_id))
267286
continue
268287
peer_expire = datetime.datetime.strptime(
269288
peer_entry.get('expire', 0), '%Y-%m-%d')
270-
user_expire = datetime.datetime.fromtimestamp(user['expire'])
271-
if peer_expire < user_expire:
289+
client_expire = datetime.datetime.fromtimestamp(user['expire'])
290+
if peer_expire < client_expire:
272291
warn_msg = "expire %s vs %s prevents %s as peer for %s" % \
273-
(peer_expire, user_expire, user_id, client_id)
292+
(peer_expire, client_expire, sponsor_id, client_id)
274293
_logger.warning(warn_msg)
275294
peer_notes.append(warn_msg)
276295
continue
277-
_logger.debug("validated %s accepts %s as peer" % (user_id,
296+
_logger.debug("validated %s accepts %s as peer" % (sponsor_id,
278297
client_id))
279-
accepted_peer_list.append(user_id)
298+
accepted_peer_list.append(sponsor_id)
280299
if not accepted_peer_list:
281300
_logger.error("requested peer validation with %r for %s failed" %
282301
(verify_pattern, client_id))

mig/shared/validstring.py

+34-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# --- BEGIN_HEADER ---
55
#
66
# validstring - string validators
7-
# Copyright (C) 2003-2018 The MiG Project lead by Brian Vinter
7+
# Copyright (C) 2003-2021 The MiG Project lead by Brian Vinter
88
#
99
# This file is part of MiG.
1010
#
@@ -26,9 +26,11 @@
2626
#
2727

2828
"""String validation"""
29+
2930
from __future__ import absolute_import
3031

3132
import os.path
33+
import re
3234

3335
from mig.shared.base import invisible_path
3436
from mig.shared.conf import get_configuration_object
@@ -100,10 +102,28 @@ def is_valid_email_address(addr, logger):
100102
if addr[c] in rfc822_specials:
101103
return False
102104
c += 1
103-
logger.debug('%s is a valid email address' % addr)
105+
logger.debug('%s is a valid email address: %s' % (addr, count >= 1))
104106
return count >= 1
105107

106108

109+
def valid_email_addresses(configuration, text, lowercase=True):
110+
"""Extract list of all valid email addresses found in free-form text"""
111+
_logger = configuration.logger
112+
# NOTE: address must end in letter(s) to avoid trailing period, etc.
113+
email_list = []
114+
all_matches = re.findall(r'[\w\._-]+@[\w\.-]+[\w]+', text)
115+
for i in all_matches:
116+
email = "%s" % i
117+
if lowercase:
118+
email = email.lower()
119+
if not is_valid_email_address(email, _logger):
120+
_logger.warning('skip invalid email: %s' % email)
121+
continue
122+
_logger.debug('found valid email: %s' % email)
123+
email_list.append(email)
124+
return email_list
125+
126+
107127
def possible_user_id(configuration, user_id):
108128
"""Check if user_id is a possible user ID based on knowledge about
109129
contents. We always use email or hexlified version of cert DN.
@@ -115,6 +135,7 @@ def possible_user_id(configuration, user_id):
115135
return False
116136
return True
117137

138+
118139
def possible_gdp_user_id(configuration, gdp_user_id):
119140
"""Check if gdp_user_id is a possible user ID based on knowledge about
120141
contents. We always use the format email@project
@@ -129,6 +150,7 @@ def possible_gdp_user_id(configuration, gdp_user_id):
129150
return False
130151
return True
131152

153+
132154
def possible_job_id(configuration, job_id):
133155
"""Check if job_id is a possible job ID based on knowledge about contents
134156
and length. We use hexlify on a random string of session_id_bytes, which
@@ -299,3 +321,13 @@ def valid_user_path(configuration, path, home_dir, allow_equal=False,
299321

300322
same = False
301323
return inside or same
324+
325+
326+
if __name__ == "__main__":
327+
from mig.shared.conf import get_configuration_object
328+
conf = get_configuration_object()
329+
comment = """Testing email extract with [email protected] and
330+
nosuchemail@abc.%!?.com and [email protected]
331+
with whatever text trailing."""
332+
print("Extract email addresses from:\n%r" % comment)
333+
print(', '.join(valid_email_addresses(conf, comment)))

0 commit comments

Comments
 (0)