Skip to content

Commit 47ff7f3

Browse files
committedNov 12, 2024
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.

File tree

6 files changed

+210
-25
lines changed

6 files changed

+210
-25
lines changed
 

‎mig/server/createuser.py

+70-21
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,7 @@ def usage(name='createuser.py'):
9191
""" % {'name': name, 'cert_warn': cert_warn})
9292

9393

94-
if '__main__' == __name__:
95-
(args, app_dir, db_path) = init_user_adm()
94+
def main(args, cwd, db_path=keyword_auto):
9695
conf_path = None
9796
auth_type = 'custom'
9897
expire = None
@@ -111,6 +110,7 @@ def usage(name='createuser.py'):
111110
user_dict = {}
112111
override_fields = {}
113112
opt_args = 'a:c:d:e:fhi:o:p:rR:s:u:v'
113+
114114
try:
115115
(opts, args) = getopt.getopt(args, opt_args)
116116
except getopt.GetoptError as err:
@@ -138,13 +138,8 @@ def usage(name='createuser.py'):
138138
parsed = True
139139
break
140140
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)
141+
print('Failed to parse expire value: %s' % val)
142+
sys.exit(1)
148143
elif opt == '-f':
149144
force = True
150145
elif opt == '-h':
@@ -154,17 +149,13 @@ def usage(name='createuser.py'):
154149
user_id = val
155150
elif opt == '-o':
156151
short_id = val
157-
override_fields['short_id'] = short_id
158152
elif opt == '-p':
159153
peer_pattern = val
160-
override_fields['peer_pattern'] = peer_pattern
161-
override_fields['status'] = 'temporal'
162154
elif opt == '-r':
163155
default_renew = True
164156
ask_renew = False
165157
elif opt == '-R':
166158
role = val
167-
override_fields['role'] = role
168159
elif opt == '-s':
169160
# Translate slack days into seconds as
170161
slack_secs = int(float(val)*24*3600)
@@ -190,8 +181,52 @@ def usage(name='createuser.py'):
190181
if verbose:
191182
print('using configuration from MIG_CONF (or default)')
192183

193-
configuration = get_configuration_object(config_file=conf_path)
184+
_main(None, args,
185+
conf_path=conf_path,
186+
db_path=db_path,
187+
expire=expire,
188+
force=force,
189+
verbose=verbose,
190+
ask_renew=ask_renew,
191+
default_renew=default_renew,
192+
ask_change_pw=ask_change_pw,
193+
user_file=user_file,
194+
user_id=user_id,
195+
short_id=short_id,
196+
role=role,
197+
peer_pattern=peer_pattern,
198+
slack_secs=slack_secs,
199+
hash_password=hash_password
200+
)
201+
202+
203+
def _main(configuration, args,
204+
conf_path=keyword_auto,
205+
db_path=keyword_auto,
206+
auth_type='custom',
207+
expire=None,
208+
force=False,
209+
verbose=False,
210+
ask_renew=True,
211+
default_renew=False,
212+
ask_change_pw=True,
213+
user_file=None,
214+
user_id=None,
215+
short_id=None,
216+
role=None,
217+
peer_pattern=None,
218+
slack_secs=0,
219+
hash_password=True
220+
):
221+
if configuration is None:
222+
if conf_path == keyword_auto:
223+
config_file = None
224+
else:
225+
config_file = conf_path
226+
configuration = get_configuration_object(config_file=config_file)
227+
194228
logger = configuration.logger
229+
195230
# NOTE: we need explicit db_path lookup here for load_user_dict call
196231
if db_path == keyword_auto:
197232
db_path = default_db_path(configuration)
@@ -211,9 +246,6 @@ def usage(name='createuser.py'):
211246
if auth_type == 'cert':
212247
hash_password = False
213248

214-
if expire is None:
215-
expire = default_account_expire(configuration, auth_type)
216-
217249
raw_user = {}
218250
if args:
219251
try:
@@ -291,9 +323,19 @@ def usage(name='createuser.py'):
291323

292324
fill_user(user_dict)
293325

294-
# Make sure account expire is set with local certificate or OpenID login
295-
326+
# assemble the fields to be explicitly overriden
327+
override_fields = {}
328+
if peer_pattern:
329+
override_fields['peer_pattern'] = peer_pattern
330+
override_fields['status'] = 'temporal'
331+
if role:
332+
override_fields['role'] = role
333+
if short_id:
334+
override_fields['short_id'] = short_id
296335
if 'expire' not in user_dict:
336+
# Make sure account expire is set with local certificate or OpenID login
337+
if not expire:
338+
expire = default_account_expire(configuration, auth_type)
297339
override_fields['expire'] = expire
298340

299341
# NOTE: let non-ID command line values override loaded values
@@ -305,8 +347,10 @@ def usage(name='createuser.py'):
305347
if verbose:
306348
print('using user dict: %s' % user_dict)
307349
try:
308-
create_user(user_dict, conf_path, db_path, force, verbose, ask_renew,
309-
default_renew, verify_peer=peer_pattern,
350+
conf_path = configuration.config_file
351+
create_user(user_dict, conf_path, db_path, configuration, force, verbose, ask_renew,
352+
default_renew,
353+
verify_peer=peer_pattern,
310354
peer_expire_slack=slack_secs, ask_change_pw=ask_change_pw)
311355
if configuration.site_enable_gdp:
312356
(success_here, msg) = ensure_gdp_user(configuration,
@@ -326,3 +370,8 @@ def usage(name='createuser.py'):
326370
if verbose:
327371
print('Cleaning up tmp file: %s' % user_file)
328372
os.remove(user_file)
373+
374+
375+
if __name__ == '__main__':
376+
(args, cwd, db_path) = init_user_adm()
377+
main(args, cwd, db_path=db_path)

‎mig/shared/accountstate.py

+1
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

+3-1
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

+7
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/useradm.py

+46-3
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,11 @@
3030
from __future__ import print_function
3131
from __future__ import absolute_import
3232

33+
from past.builtins import basestring
3334
from email.utils import parseaddr
35+
import codecs
3436
import datetime
37+
import errno
3538
import fnmatch
3639
import os
3740
import re
@@ -44,6 +47,7 @@
4447
from mig.shared.base import client_id_dir, client_dir_id, client_alias, \
4548
get_client_id, extract_field, fill_user, fill_distinguished_name, \
4649
is_gdp_user, mask_creds, sandbox_resource
50+
from mig.shared.compat import _unicode_string_to_escaped_unicode
4751
from mig.shared.conf import get_configuration_object
4852
from mig.shared.configuration import Configuration
4953
from mig.shared.defaults import user_db_filename, keyword_auto, ssh_conf_dir, \
@@ -97,6 +101,10 @@
97101
https_authdigests = user_db_filename
98102

99103

104+
_USERADM_CONFIG_DIR_KEYS = ('user_db_home', 'user_home', 'user_settings',
105+
'user_cache', 'mrsl_files_dir', 'resource_pending')
106+
107+
100108
def init_user_adm(dynamic_db_path=True):
101109
"""Shared init function for all user administration scripts.
102110
The optional dynamic_db_path argument toggles dynamic user db path lookup
@@ -451,6 +459,21 @@ def verify_user_peers(configuration, db_path, client_id, user, now, verify_peer,
451459
return accepted_peer_list, effective_expire
452460

453461

462+
def _check_directories_unprovisioned(configuration, db_path):
463+
user_db_home = os.path.dirname(db_path)
464+
return not os.path.exists(db_path) and not os.path.exists(user_db_home)
465+
466+
467+
def _provision_directories(configuration):
468+
for config_attr in _USERADM_CONFIG_DIR_KEYS:
469+
try:
470+
dir_to_create = getattr(configuration, config_attr)
471+
os.mkdir(dir_to_create)
472+
except OSError as oserr:
473+
if oserr.errno != errno.ENOENT: # FileNotFoundError
474+
raise
475+
476+
454477
def create_user_in_db(configuration, db_path, client_id, user, now, authorized,
455478
reset_token, reset_auth_type, accepted_peer_list, force,
456479
verbose, ask_renew, default_renew, do_lock,
@@ -463,8 +486,25 @@ def create_user_in_db(configuration, db_path, client_id, user, now, authorized,
463486
flock = None
464487
user_db = {}
465488
renew = default_renew
489+
490+
retry_lock = False
466491
if do_lock:
492+
try:
493+
flock = lock_user_db(db_path)
494+
except (IOError, OSError) as oserr:
495+
if oserr.errno != errno.ENOENT: # FileNotFoundError
496+
raise
497+
498+
if _check_directories_unprovisioned(configuration, db_path=db_path):
499+
_provision_directories(configuration)
500+
retry_lock = True
501+
else:
502+
raise Exception("Failed to lock user DB: '%s'" % db_path)
503+
504+
if retry_lock:
467505
flock = lock_user_db(db_path)
506+
if not flock:
507+
raise Exception("Failed to lock user DB: '%s'" % db_path)
468508

469509
if not os.path.exists(db_path):
470510
# Auto-create missing user DB if either auto_create_db or force is set
@@ -859,7 +899,7 @@ def create_user_in_fs(configuration, client_id, user, now, renew, force, verbose
859899
# match in htaccess
860900

861901
dn_plain = info['distinguished_name']
862-
dn_enc = dn_plain.encode('string_escape')
902+
dn_enc = _unicode_string_to_escaped_unicode(dn_plain)
863903

864904
def upper_repl(match):
865905
"""Translate hex codes to upper case form"""
@@ -1013,15 +1053,18 @@ def upper_repl(match):
10131053
raise Exception('could not create custom css file: %s' % css_path)
10141054

10151055

1016-
def create_user(user, conf_path, db_path, force=False, verbose=False,
1056+
def create_user(user, conf_path, db_path, configuration=None, force=False, verbose=False,
10171057
ask_renew=True, default_renew=False, do_lock=True,
10181058
verify_peer=None, peer_expire_slack=0, from_edit_user=False,
10191059
ask_change_pw=False, auto_create_db=True, create_backup=True):
10201060
"""Add user in database and in file system. Distinguishes on the user ID
10211061
format as a first step.
10221062
"""
10231063

1024-
if conf_path:
1064+
if configuration is not None:
1065+
# use it
1066+
pass
1067+
elif conf_path:
10251068
if isinstance(conf_path, basestring):
10261069

10271070
# has been checked for accessibility above...

‎tests/test_mig_server_createuser.py

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# --- BEGIN_HEADER ---
4+
#
5+
# test_mig_server-createuser - unit tests for the migrid createuser CLI
6+
# Copyright (C) 2003-2024 The MiG Project by the Science HPC Center at UCPH
7+
#
8+
# This file is part of MiG.
9+
#
10+
# MiG is free software: you can redistribute it and/or modify
11+
# it under the terms of the GNU General Public License as published by
12+
# the Free Software Foundation; either version 2 of the License, or
13+
# (at your option) any later version.
14+
#
15+
# MiG is distributed in the hope that it will be useful,
16+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
17+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+
# GNU General Public License for more details.
19+
#
20+
# You should have received a copy of the GNU General Public License
21+
# along with this program; if not, write to the Free Software
22+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
23+
# USA.
24+
#
25+
# --- END_HEADER ---
26+
#
27+
28+
"""Unit tests for the migrid createuser CLI"""
29+
30+
from __future__ import print_function
31+
import os
32+
import shutil
33+
import sys
34+
35+
from tests.support import MIG_BASE, TEST_OUTPUT_DIR, MigTestCase, testmain
36+
37+
from mig.server.createuser import _main as createuser
38+
from mig.shared.useradm import _USERADM_CONFIG_DIR_KEYS
39+
40+
41+
class TestBooleans(MigTestCase):
42+
def before_each(self):
43+
configuration = self.configuration
44+
test_state_path = configuration.state_path
45+
46+
for config_key in _USERADM_CONFIG_DIR_KEYS:
47+
dir_path = getattr(configuration, config_key)[0:-1]
48+
try:
49+
shutil.rmtree(dir_path)
50+
except:
51+
pass
52+
53+
self.expected_user_db_home = configuration.user_db_home[0:-1]
54+
55+
def _provide_configuration(self):
56+
return 'testconfig'
57+
58+
def test_user_db_is_created_and_user_is_added(self):
59+
args = [
60+
"Test User",
61+
"Test Org",
62+
"NA",
63+
"DK",
64+
"dummy-user",
65+
"This is the create comment",
66+
"password"
67+
]
68+
print("") # acount for output generated by the logic
69+
createuser(self.configuration, args, default_renew=True)
70+
71+
# presence of user home
72+
path_kind = MigTestCase._absolute_path_kind(self.expected_user_db_home)
73+
self.assertEqual(path_kind, 'dir')
74+
75+
# presence of user db
76+
expected_user_db_file = os.path.join(
77+
self.expected_user_db_home, 'MiG-users.db')
78+
path_kind = MigTestCase._absolute_path_kind(expected_user_db_file)
79+
self.assertEqual(path_kind, 'file')
80+
81+
82+
if __name__ == '__main__':
83+
testmain()

0 commit comments

Comments
 (0)