Skip to content

Commit ca3c4f2

Browse files
committed
Make create_user 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 creat_user ensuring that a user db is created (along with its lock file) and that a user entry is added to the database.
1 parent c545adc commit ca3c4f2

File tree

5 files changed

+236
-11
lines changed

5 files changed

+236
-11
lines changed

mig/shared/useradm.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@
3535
from past.builtins import basestring
3636

3737
from email.utils import parseaddr
38+
import codecs
3839
import datetime
40+
import errno
3941
import fnmatch
4042
import os
4143
import re
@@ -102,6 +104,10 @@
102104
https_authdigests = user_db_filename
103105

104106

107+
_USERADM_CONFIG_DIR_KEYS = ('user_db_home', 'user_home', 'user_settings',
108+
'user_cache', 'mrsl_files_dir', 'resource_pending')
109+
110+
105111
def init_user_adm(dynamic_db_path=True):
106112
"""Shared init function for all user administration scripts.
107113
The optional dynamic_db_path argument toggles dynamic user db path lookup
@@ -458,6 +464,21 @@ def verify_user_peers(configuration, db_path, client_id, user, now, verify_peer,
458464
return accepted_peer_list, effective_expire
459465

460466

467+
def _check_directories_unprovisioned(configuration, db_path):
468+
user_db_home = os.path.dirname(db_path)
469+
return not os.path.exists(db_path) and not os.path.exists(user_db_home)
470+
471+
472+
def _provision_directories(configuration):
473+
for config_attr in _USERADM_CONFIG_DIR_KEYS:
474+
try:
475+
dir_to_create = getattr(configuration, config_attr)
476+
os.mkdir(dir_to_create)
477+
except OSError as oserr:
478+
if oserr.errno != errno.ENOENT: # FileNotFoundError
479+
raise
480+
481+
461482
def create_user_in_db(configuration, db_path, client_id, user, now, authorized,
462483
reset_token, reset_auth_type, accepted_peer_list, force,
463484
verbose, ask_renew, default_renew, do_lock,
@@ -470,8 +491,25 @@ def create_user_in_db(configuration, db_path, client_id, user, now, authorized,
470491
flock = None
471492
user_db = {}
472493
renew = default_renew
494+
495+
retry_lock = False
473496
if do_lock:
497+
try:
498+
flock = lock_user_db(db_path)
499+
except (IOError, OSError) as oserr:
500+
if oserr.errno != errno.ENOENT: # FileNotFoundError
501+
raise
502+
503+
if _check_directories_unprovisioned(configuration, db_path=db_path):
504+
_provision_directories(configuration)
505+
retry_lock = True
506+
else:
507+
raise Exception("Failed to lock user DB: '%s'" % db_path)
508+
509+
if retry_lock:
474510
flock = lock_user_db(db_path)
511+
if not flock:
512+
raise Exception("Failed to lock user DB: '%s'" % db_path)
475513

476514
if not os.path.exists(db_path):
477515
# Auto-create missing user DB if either auto_create_db or force is set
@@ -1027,7 +1065,9 @@ def create_user(user, conf_path, db_path, force=False, verbose=False,
10271065
format as a first step.
10281066
"""
10291067

1030-
if conf_path:
1068+
if isinstance(conf_path, Configuration):
1069+
configuration = conf_path
1070+
elif conf_path:
10311071
if isinstance(conf_path, basestring):
10321072

10331073
# has been checked for accessibility above...

tests/support/__init__.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
from unittest import TestCase, main as testmain
4242

4343
from tests.support.configsupp import FakeConfiguration
44+
from tests.support.pathsupp import is_path_within
4445
from tests.support.suppconst import MIG_BASE, TEST_BASE, TEST_FIXTURE_DIR, \
4546
TEST_DATA_DIR, TEST_OUTPUT_DIR, ENVHELP_OUTPUT_DIR
4647

@@ -296,16 +297,6 @@ def pretty_display_path(absolute_path):
296297
return relative_path
297298

298299

299-
def is_path_within(path, start=None, _msg=None):
300-
"""Check if path is within start directory"""
301-
try:
302-
assert os.path.isabs(path), _msg
303-
relative = os.path.relpath(path, start=start)
304-
except:
305-
return False
306-
return not relative.startswith('..')
307-
308-
309300
def ensure_dirs_exist(absolute_dir):
310301
"""A simple helper to create absolute_dir and any parents if missing"""
311302
try:

tests/support/pathsupp.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/usr/bin/python
2+
# -*- coding: utf-8 -*-
3+
#
4+
# --- BEGIN_HEADER ---
5+
#
6+
# __init__ - package marker and core package functions
7+
# Copyright (C) 2003-2024 The MiG Project by the Science HPC Center at UCPH
8+
#
9+
# This file is part of MiG.
10+
#
11+
# MiG is free software: you can redistribute it and/or modify
12+
# it under the terms of the GNU General Public License as published by
13+
# the Free Software Foundation; either version 2 of the License, or
14+
# (at your option) any later version.
15+
#
16+
# MiG is distributed in the hope that it will be useful,
17+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
18+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19+
# GNU General Public License for more details.
20+
#
21+
# You should have received a copy of the GNU General Public License
22+
# along with this program; if not, write to the Free Software
23+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
24+
#
25+
# -- END_HEADER ---
26+
#
27+
28+
"""Path related details within the test support library."""
29+
30+
import os
31+
32+
33+
def is_path_within(path, start=None, _msg=None):
34+
"""Check if path is within start directory"""
35+
try:
36+
assert os.path.isabs(path), _msg
37+
relative = os.path.relpath(path, start=start)
38+
except AssertionError:
39+
return False
40+
return not relative.startswith('..')

tests/support/picklesupp.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import pickle
2+
3+
from tests.support.suppconst import TEST_OUTPUT_DIR
4+
from tests.support.pathsupp import is_path_within
5+
6+
7+
class PickleAssertMixin:
8+
def assertPickledFile(self, pickle_file_path):
9+
assert is_path_within(pickle_file_path, TEST_OUTPUT_DIR)
10+
11+
with open(pickle_file_path, 'rb') as picklefile:
12+
return pickle.load(picklefile)

tests/test_mig_shared_usreadm.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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 module pointed to in the filename"""
29+
30+
from __future__ import print_function
31+
import codecs
32+
import os
33+
import shutil
34+
import sys
35+
import unittest
36+
37+
from tests.support import MIG_BASE, TEST_OUTPUT_DIR, MigTestCase, testmain
38+
from tests.support.picklesupp import PickleAssertMixin
39+
40+
from mig.shared.base import keyword_auto
41+
from mig.shared.useradm import create_user
42+
43+
_USERADM_CONFIG_DIR_KEYS = ('user_db_home', 'user_home', 'user_settings',
44+
'user_cache', 'mrsl_files_dir', 'resource_pending')
45+
46+
47+
def _as_bytes(value):
48+
if isinstance(value, str):
49+
return codecs.encode(value, 'utf8')
50+
else:
51+
return value
52+
53+
54+
class TestMigSharedUsedadm_create_user(MigTestCase, PickleAssertMixin):
55+
def before_each(self):
56+
configuration = self.configuration
57+
test_state_path = configuration.state_path
58+
59+
for config_key in _USERADM_CONFIG_DIR_KEYS:
60+
dir_path = getattr(configuration, config_key)[0:-1]
61+
try:
62+
shutil.rmtree(dir_path)
63+
except:
64+
pass
65+
66+
self.expected_user_db_home = configuration.user_db_home[0:-1]
67+
self.expected_user_db_file = os.path.join(
68+
self.expected_user_db_home, 'MiG-users.db')
69+
70+
def _provide_configuration(self):
71+
return 'testconfig'
72+
73+
def test_user_db_is_created(self):
74+
user_dict = {}
75+
user_dict['full_name'] = "Test User"
76+
user_dict['organization'] = "Test Org"
77+
user_dict['state'] = "NA"
78+
user_dict['country'] = "DK"
79+
user_dict['email'] = "user@example.com"
80+
user_dict['comment'] = "This is the create comment"
81+
user_dict['password'] = "password"
82+
create_user(user_dict, self.configuration,
83+
keyword_auto, default_renew=True)
84+
85+
# presence of user home
86+
path_kind = MigTestCase._absolute_path_kind(self.expected_user_db_home)
87+
self.assertEqual(path_kind, 'dir')
88+
89+
# presence of user db
90+
path_kind = MigTestCase._absolute_path_kind(self.expected_user_db_file)
91+
self.assertEqual(path_kind, 'file')
92+
93+
def test_user_entry_is_recorded(self):
94+
def _generate_salt():
95+
return b'CCCC12344321CCCC'
96+
97+
expected_user_id = '/C=DK/ST=NA/L=NA/O=Test Org/OU=NA/CN=Test User/emailAddress=user@example.com'
98+
expected_user_unique_id = 'OUejmKGMFWPWLyi5chqQalxDgltTuG1SoZUsqGyj32yY3275GjA2GfMo5odeWuKQ'
99+
expected_user_password_hash = "PBKDF2$sha256$10000$b'CCCC12344321CCCC'$b'bph8p/avUq42IYeOdJoJuUqrJ7Q32eaT'"
100+
101+
user_dict = {}
102+
user_dict['full_name'] = "Test User"
103+
user_dict['organization'] = "Test Org"
104+
user_dict['state'] = "NA"
105+
user_dict['country'] = "DK"
106+
user_dict['email'] = "user@example.com"
107+
user_dict['comment'] = "This is the create comment"
108+
user_dict['password'] = "password"
109+
110+
create_user(user_dict, self.configuration,
111+
keyword_auto, default_renew=True)
112+
113+
pickled = self.assertPickledFile(self.expected_user_db_file)
114+
# FIXME: Python3 pickle appears to be keyed by bytes
115+
picked_expected_user_id = _as_bytes(expected_user_id)
116+
self.assertIn(picked_expected_user_id, pickled)
117+
118+
actual_user_object = pickled[picked_expected_user_id]
119+
120+
# TODO: remove resetting the handful of keys here done because changes
121+
# to make them assertion frienfly values will increase the size
122+
# of the diff which, at time of commit, are best minimised.
123+
actual_user_object[b'created'] = 9999999999.9999999
124+
actual_user_object[b'unique_id'] = '__UNIQUE_ID__'
125+
126+
self.assertEqual(actual_user_object, {
127+
b'full_name': b'Test User',
128+
b'organization': b'Test Org',
129+
b'state': b'NA',
130+
b'country': b'DK',
131+
b'email': b'user@example.com',
132+
b'comment': b'This is the create comment',
133+
b'password': b'password',
134+
b'distinguished_name': picked_expected_user_id,
135+
b'created': 9999999999.9999999,
136+
b'unique_id': '__UNIQUE_ID__',
137+
b'openid_names': [],
138+
})
139+
140+
141+
if __name__ == '__main__':
142+
testmain()

0 commit comments

Comments
 (0)