Skip to content

Commit 34ea019

Browse files
committed
Make createuser work for new installations and add basic test.
Allow user creation to provision the directries necessary for user creation to succeed in addition to the database file itself given the state that exists after config generation is run to completion for a new installation. Cover the very basic operation of createuser ensuring that a user db and lock file are correctly create upon a call to create a test user.
1 parent a17c11a commit 34ea019

File tree

15 files changed

+409
-77
lines changed

15 files changed

+409
-77
lines changed

mig/server/createuser.py

Lines changed: 94 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from builtins import input
3434
from getpass import getpass
3535
import datetime
36+
import errno
3637
import getopt
3738
import os
3839
import sys
@@ -91,8 +92,7 @@ def usage(name='createuser.py'):
9192
""" % {'name': name, 'cert_warn': cert_warn})
9293

9394

94-
if '__main__' == __name__:
95-
(args, app_dir, db_path) = init_user_adm()
95+
def main(_main, args, cwd, db_path=keyword_auto):
9696
conf_path = None
9797
auth_type = 'custom'
9898
expire = None
@@ -111,6 +111,7 @@ def usage(name='createuser.py'):
111111
user_dict = {}
112112
override_fields = {}
113113
opt_args = 'a:c:d:e:fhi:o:p:rR:s:u:v'
114+
114115
try:
115116
(opts, args) = getopt.getopt(args, opt_args)
116117
except getopt.GetoptError as err:
@@ -138,13 +139,8 @@ def usage(name='createuser.py'):
138139
parsed = True
139140
break
140141
except ValueError:
141-
pass
142-
if parsed:
143-
override_fields['expire'] = expire
144-
override_fields['status'] = 'temporal'
145-
else:
146-
print('Failed to parse expire value: %s' % val)
147-
sys.exit(1)
142+
print('Failed to parse expire value: %s' % val)
143+
sys.exit(1)
148144
elif opt == '-f':
149145
force = True
150146
elif opt == '-h':
@@ -154,17 +150,13 @@ def usage(name='createuser.py'):
154150
user_id = val
155151
elif opt == '-o':
156152
short_id = val
157-
override_fields['short_id'] = short_id
158153
elif opt == '-p':
159154
peer_pattern = val
160-
override_fields['peer_pattern'] = peer_pattern
161-
override_fields['status'] = 'temporal'
162155
elif opt == '-r':
163156
default_renew = True
164157
ask_renew = False
165158
elif opt == '-R':
166159
role = val
167-
override_fields['role'] = role
168160
elif opt == '-s':
169161
# Translate slack days into seconds as
170162
slack_secs = int(float(val)*24*3600)
@@ -178,7 +170,12 @@ def usage(name='createuser.py'):
178170
print('Error: %s not supported!' % opt)
179171
sys.exit(1)
180172

181-
if conf_path and not os.path.isfile(conf_path):
173+
if not conf_path:
174+
# explicitly set the default value of keyword_auto if no option was
175+
# provided since it is unconditionally passed inward as a keyword arg
176+
# and thus the fallback would accidentally be ignored
177+
conf_path = keyword_auto
178+
elif not os.path.isfile(conf_path):
182179
print('Failed to read configuration file: %s' % conf_path)
183180
sys.exit(1)
184181

@@ -190,30 +187,76 @@ def usage(name='createuser.py'):
190187
if verbose:
191188
print('using configuration from MIG_CONF (or default)')
192189

193-
configuration = get_configuration_object(config_file=conf_path)
190+
ret = _main(None, args,
191+
conf_path=conf_path,
192+
db_path=db_path,
193+
expire=expire,
194+
force=force,
195+
verbose=verbose,
196+
ask_renew=ask_renew,
197+
default_renew=default_renew,
198+
ask_change_pw=ask_change_pw,
199+
user_file=user_file,
200+
user_id=user_id,
201+
short_id=short_id,
202+
role=role,
203+
peer_pattern=peer_pattern,
204+
slack_secs=slack_secs,
205+
hash_password=hash_password
206+
)
207+
208+
if ret == errno.ENOTSUP:
209+
usage()
210+
sys.exit(1)
211+
212+
sys.exit(ret)
213+
214+
215+
def _main(configuration, args,
216+
conf_path=keyword_auto,
217+
db_path=keyword_auto,
218+
auth_type='custom',
219+
expire=None,
220+
force=False,
221+
verbose=False,
222+
ask_renew=True,
223+
default_renew=False,
224+
ask_change_pw=True,
225+
user_file=None,
226+
user_id=None,
227+
short_id=None,
228+
role=None,
229+
peer_pattern=None,
230+
slack_secs=0,
231+
hash_password=True,
232+
_generate_salt=None
233+
):
234+
if configuration is None:
235+
if conf_path == keyword_auto:
236+
config_file = None
237+
else:
238+
config_file = conf_path
239+
configuration = get_configuration_object(config_file=config_file)
240+
194241
logger = configuration.logger
242+
195243
# NOTE: we need explicit db_path lookup here for load_user_dict call
196244
if db_path == keyword_auto:
197245
db_path = default_db_path(configuration)
198246

199247
if user_file and args:
200248
print('Error: Only one kind of user specification allowed at a time')
201-
usage()
202-
sys.exit(1)
249+
return errno.ENOTSUP
203250

204251
if auth_type not in valid_auth_types:
205252
print('Error: invalid account auth type %r requested (allowed: %s)' %
206253
(auth_type, ', '.join(valid_auth_types)))
207-
usage()
208-
sys.exit(1)
254+
return errno.ENOTSUP
209255

210256
# NOTE: renew requires original password
211257
if auth_type == 'cert':
212258
hash_password = False
213259

214-
if expire is None:
215-
expire = default_account_expire(configuration, auth_type)
216-
217260
raw_user = {}
218261
if args:
219262
try:
@@ -229,8 +272,7 @@ def usage(name='createuser.py'):
229272
except IndexError:
230273
print('Error: too few arguments given (expected 7 got %d)'
231274
% len(args))
232-
usage()
233-
sys.exit(1)
275+
return errno.ENOTSUP
234276
# Force user ID fields to canonical form for consistency
235277
# Title name, lowercase email, uppercase country and state, etc.
236278
user_dict = canonical_user(configuration, raw_user, raw_user.keys())
@@ -239,14 +281,12 @@ def usage(name='createuser.py'):
239281
user_dict = load(user_file)
240282
except Exception as err:
241283
print('Error in user name extraction: %s' % err)
242-
usage()
243-
sys.exit(1)
284+
return errno.ENOTSUP
244285
elif default_renew and user_id:
245286
saved = load_user_dict(logger, user_id, db_path, verbose)
246287
if not saved:
247288
print('Error: no such user in user db: %s' % user_id)
248-
usage()
249-
sys.exit(1)
289+
return errno.ENOTSUP
250290
user_dict.update(saved)
251291
del user_dict['expire']
252292
elif not configuration.site_enable_gdp:
@@ -268,13 +308,13 @@ def usage(name='createuser.py'):
268308
print("Error: Missing one or more of the arguments: "
269309
+ "[FULL_NAME] [ORGANIZATION] [STATE] [COUNTRY] "
270310
+ "[EMAIL] [COMMENT] [PASSWORD]")
271-
sys.exit(1)
311+
return 1
272312

273313
# Encode password if set but not already encoded
274314

275315
if user_dict['password']:
276316
if hash_password:
277-
user_dict['password_hash'] = make_hash(user_dict['password'])
317+
user_dict['password_hash'] = make_hash(user_dict['password'], _generate_salt=_generate_salt)
278318
user_dict['password'] = ''
279319
else:
280320
salt = configuration.site_password_salt
@@ -291,9 +331,19 @@ def usage(name='createuser.py'):
291331

292332
fill_user(user_dict)
293333

294-
# Make sure account expire is set with local certificate or OpenID login
295-
334+
# assemble the fields to be explicitly overriden
335+
override_fields = {}
336+
if peer_pattern:
337+
override_fields['peer_pattern'] = peer_pattern
338+
override_fields['status'] = 'temporal'
339+
if role:
340+
override_fields['role'] = role
341+
if short_id:
342+
override_fields['short_id'] = short_id
296343
if 'expire' not in user_dict:
344+
# Make sure account expire is set with local certificate or OpenID login
345+
if not expire:
346+
expire = default_account_expire(configuration, auth_type)
297347
override_fields['expire'] = expire
298348

299349
# NOTE: let non-ID command line values override loaded values
@@ -305,8 +355,10 @@ def usage(name='createuser.py'):
305355
if verbose:
306356
print('using user dict: %s' % user_dict)
307357
try:
308-
create_user(user_dict, conf_path, db_path, force, verbose, ask_renew,
309-
default_renew, verify_peer=peer_pattern,
358+
conf_path = configuration.config_file
359+
create_user(user_dict, conf_path, db_path, configuration, force, verbose, ask_renew,
360+
default_renew,
361+
verify_peer=peer_pattern,
310362
peer_expire_slack=slack_secs, ask_change_pw=ask_change_pw)
311363
if configuration.site_enable_gdp:
312364
(success_here, msg) = ensure_gdp_user(configuration,
@@ -319,10 +371,17 @@ def usage(name='createuser.py'):
319371
print("Error creating user: %s" % exc)
320372
import traceback
321373
logger.warning("Error creating user: %s" % traceback.format_exc())
322-
sys.exit(1)
374+
return 1
323375
print('Created or updated %s in user database and in file system' %
324376
user_dict['distinguished_name'])
325377
if user_file:
326378
if verbose:
327379
print('Cleaning up tmp file: %s' % user_file)
328380
os.remove(user_file)
381+
382+
return 0
383+
384+
385+
if __name__ == '__main__':
386+
(args, cwd, db_path) = init_user_adm()
387+
main(_main, args, cwd, db_path=db_path)

mig/shared/_env.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import os
2+
3+
MIG_BASE = os.path.realpath(os.path.join(os.path.dirname(__file__), '../..'))
4+
MIG_ENV = os.getenv('MIG_ENV', 'default')

mig/shared/accountstate.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from __future__ import absolute_import
3434
from past.builtins import basestring
3535

36+
from past.builtins import basestring
3637
import os
3738
import time
3839

mig/shared/base.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,9 @@ def canonical_user(configuration, user_dict, limit_fields):
295295
if key == 'full_name':
296296
# IMPORTANT: we get utf8 coded bytes here and title() treats such
297297
# chars as word termination. Temporarily force to unicode.
298-
val = force_utf8(force_unicode(val).title())
298+
val = force_unicode(val).title()
299+
if PY2:
300+
val = force_utf8(val)
299301
elif key == 'email':
300302
val = val.lower()
301303
elif key == 'country':

mig/shared/compat.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ def _is_unicode(val):
5555
return (type(val) == _TYPE_UNICODE)
5656

5757

58+
def _unicode_string_to_escaped_unicode(unicode_string):
59+
"""Convert utf8 bytes to escaped unicode string."""
60+
61+
utf8_bytes = dn_utf8_bytes = codecs.encode(unicode_string, 'utf8')
62+
return codecs.decode(utf8_bytes, 'unicode_escape')
63+
64+
5865
def ensure_native_string(string_or_bytes):
5966
"""Given a supplied input which can be either a string or bytes
6067
return a representation providing string operations while ensuring that

mig/shared/conf.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import os
3333
import sys
3434

35+
from mig.shared._env import MIG_BASE
3536
from mig.shared.fileio import unpickle
3637

3738

@@ -43,16 +44,14 @@ def get_configuration_object(config_file=None, skip_log=False,
4344
"""
4445
from mig.shared.configuration import Configuration
4546
if config_file:
47+
# use config file path passed explicitly to the function
4648
_config_file = config_file
4749
elif os.environ.get('MIG_CONF', None):
50+
# use config file explicitly set in the environment
4851
_config_file = os.environ['MIG_CONF']
4952
else:
50-
app_dir = os.path.dirname(sys.argv[0])
51-
if not app_dir:
52-
_config_file = '../server/MiGserver.conf'
53-
else:
54-
_config_file = os.path.join(app_dir, '..', 'server',
55-
'MiGserver.conf')
53+
# find config file relative to the directory in which the scrip resides
54+
_config_file = os.path.join(MIG_BASE, 'server/MiGserver.conf')
5655
configuration = Configuration(_config_file, False, skip_log,
5756
disable_auth_log)
5857
return configuration
@@ -61,7 +60,7 @@ def get_configuration_object(config_file=None, skip_log=False,
6160
def get_resource_configuration(resource_home, unique_resource_name,
6261
logger):
6362
"""Load a resource configuration from file"""
64-
63+
6564
# open the configuration file
6665

6766
resource_config_file = resource_home + '/' + unique_resource_name\

mig/shared/configuration.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -737,7 +737,7 @@ def reload_config(self, verbose, skip_log=False, disable_auth_log=False,
737737
else:
738738
print("""Could not find your configuration file (%s). You might
739739
need to point the MIG_CONF environment to your actual MiGserver.conf
740-
location.""" % self.config_file)
740+
location.""" % _config_file)
741741
raise IOError
742742

743743
config = ConfigParser()

mig/shared/defaults.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,7 @@
3232
import os
3333
import sys
3434

35-
MIG_BASE = os.path.realpath(os.path.join(os.path.dirname(__file__), '../..'))
36-
MIG_ENV = os.getenv('MIG_ENV', 'default')
35+
from mig.shared._env import MIG_BASE, MIG_ENV
3736

3837
# NOTE: python3 switched strings to use unicode by default in contrast to bytes
3938
# in python2. File systems remain with utf8 however so we need to

mig/shared/pwcrypto.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,17 @@ def best_crypt_salt(configuration):
114114
return salt_data
115115

116116

117-
def make_hash(password):
117+
def _random_salt():
118+
return b64encode(urandom(SALT_LENGTH))
119+
120+
121+
def make_hash(password, _generate_salt=None):
118122
"""Generate a random salt and return a new hash for the password."""
119-
salt = b64encode(urandom(SALT_LENGTH))
123+
124+
if _generate_salt is None:
125+
_generate_salt = _random_salt
126+
127+
salt = _generate_salt()
120128
derived = b64encode(hashlib.pbkdf2_hmac(HASH_FUNCTION,
121129
force_utf8(password), salt,
122130
COST_FACTOR, KEY_LENGTH))

0 commit comments

Comments
 (0)