From fda86ce7105875093579ba4872bf11761b54323b Mon Sep 17 00:00:00 2001 From: Brendan Reilly Date: Mon, 23 Apr 2018 16:06:40 -0400 Subject: [PATCH] Added editUser api call Fixes: #862 --- cli/koji_cli/commands.py | 17 +++ hub/kojihub.py | 37 +++++++ tests/test_cli/data/list-commands-admin.txt | 1 + tests/test_cli/data/list-commands.txt | 1 + tests/test_cli/test_edit_user.py | 114 ++++++++++++++++++++ tests/test_hub/test_edit_user.py | 60 +++++++++++ 6 files changed, 230 insertions(+) create mode 100644 tests/test_cli/test_edit_user.py create mode 100644 tests/test_hub/test_edit_user.py diff --git a/cli/koji_cli/commands.py b/cli/koji_cli/commands.py index fc13af73..0d572bf8 100644 --- a/cli/koji_cli/commands.py +++ b/cli/koji_cli/commands.py @@ -2058,6 +2058,23 @@ def handle_disable_user(goptions, session, args): session.disableUser(username) +def handle_edit_user(goptions, session, args): + "[admin] Alter user information" + usage = _("usage: %prog edit-user name [options]") + usage += _("\n(Specify the --help global option for a list of other help options)") + parser = OptionParser(usage=usage) + parser.add_option("--rename", help=_("Rename the user")) + parser.add_option("--krb", help=_("Change kerberos principal of the user")) + (options, args) = parser.parse_args(args) + if len(args) < 1: + parser.error(_("You must specify the username of the user to edit")) + elif len(args) > 1: + parser.error(_("This command only accepts one argument (username)")) + activate_session(session, goptions) + user = args[0] + session.editUser(user, options.rename, options.krb) + + def handle_list_signed(goptions, session, args): "[admin] List signed copies of rpms" usage = _("usage: %prog list-signed [options]") diff --git a/hub/kojihub.py b/hub/kojihub.py index 2e7fff12..bcf2a2d4 100644 --- a/hub/kojihub.py +++ b/hub/kojihub.py @@ -3701,6 +3701,42 @@ def get_user(userInfo=None, strict=False, krb_princs=False): user['krb_principals'] = list_user_krb_principals(user['id']) return user +def edit_user(userInfo, name=None, krb_principal=None): + """Edit information for an existing user. + + userInfo specifies the user to edit + fields changes are provided as keyword arguments: + name: rename the user + krb_principal: change user's kerberos principal + """ + + context.session.assertPerm('admin') + _edit_user(userInfo, name=name, krb_principal=krb_principal) + + +def _edit_user(userInfo, name=None, krb_principal=None): + """Edit information for an existing user.""" + user = get_user(userInfo, strict=True) + if name and user['name'] != name: + # attempt to update user name + values = { + 'name': name, + 'userID': user['id'] + } + q = """SELECT id FROM users WHERE name=%(name)s""" + id = _singleValue(q, values, strict=False) + if id is not None: + # new name is taken + raise koji.GenericError("Name %s already taken by user %s" % (name, id)) + update = UpdateProcessor('users', values={'userID': user['id']}, clauses=['id = %(userID)i']) + update.set(name=name) + update.execute() + if krb_principal and user['krb_principal'] != krb_principal: + # attempt to update kerberos principal + update = UpdateProcessor('users', values={'userID': user['id']}, clauses=['id = %(userID)i']) + update.set(krb_principal=krb_principal) + update.execute() + def list_user_krb_principals(user_info=None): """Return kerberos principal list of a user. @@ -10942,6 +10978,7 @@ class RootExports(object): return pkgs.get(pkg_id, None) getUser = staticmethod(get_user) + editUser = staticmethod(edit_user) def grantPermission(self, userinfo, permission, create=False): """Grant a permission to a user""" diff --git a/tests/test_cli/data/list-commands-admin.txt b/tests/test_cli/data/list-commands-admin.txt index d94761a2..b0f3fdc1 100644 --- a/tests/test_cli/data/list-commands-admin.txt +++ b/tests/test_cli/data/list-commands-admin.txt @@ -26,6 +26,7 @@ admin commands: edit-tag Alter tag information edit-tag-inheritance Edit tag inheritance edit-target Set the name, build_tag, and/or dest_tag of an existing build target to new values + edit-user Alter user information enable-host Mark one or more hosts as enabled enable-user Enable logins by a user free-task Free a task diff --git a/tests/test_cli/data/list-commands.txt b/tests/test_cli/data/list-commands.txt index 12bac1b8..0e9d5837 100644 --- a/tests/test_cli/data/list-commands.txt +++ b/tests/test_cli/data/list-commands.txt @@ -26,6 +26,7 @@ admin commands: edit-tag Alter tag information edit-tag-inheritance Edit tag inheritance edit-target Set the name, build_tag, and/or dest_tag of an existing build target to new values + edit-user Alter user information enable-host Mark one or more hosts as enabled enable-user Enable logins by a user free-task Free a task diff --git a/tests/test_cli/test_edit_user.py b/tests/test_cli/test_edit_user.py new file mode 100644 index 00000000..1dcd497b --- /dev/null +++ b/tests/test_cli/test_edit_user.py @@ -0,0 +1,114 @@ +from __future__ import absolute_import +import mock +import os +import six +import sys +import unittest + + +from koji_cli.commands import handle_edit_user + +progname = os.path.basename(sys.argv[0]) or 'koji' + + +class TestEditUser(unittest.TestCase): + # Show long diffs in error output... + maxDiff = None + + @mock.patch('sys.stdout', new_callable=six.StringIO) + @mock.patch('koji_cli.commands.activate_session') + def test_handle_edit_user(self, activate_session_mock, stdout): + user = 'user' + rename = 'user2' + krb_principal = 'krb' + args = [user] + args.append('--rename=' + rename) + args.append('--krb=' + krb_principal) + options = mock.MagicMock() + + # Mock out the xmlrpc server + session = mock.MagicMock() + + # Run it and check immediate output + # args: user --rename=user --krb=krb + # expected: success + rv = handle_edit_user(options, session, args) + actual = stdout.getvalue() + expected = '' + self.assertMultiLineEqual(actual, expected) + # Finally, assert that things were called as we expected. + activate_session_mock.assert_called_once_with(session, options) + session.editUser.assert_called_once_with(user, rename, krb_principal) + self.assertEqual(rv, None) + + stdout.seek(0) + stdout.truncate() + session.reset_mock() + activate_session_mock.reset_mock() + + @mock.patch('sys.stdout', new_callable=six.StringIO) + @mock.patch('sys.stderr', new_callable=six.StringIO) + @mock.patch('koji_cli.commands.activate_session') + def test_handle_edit_user_help(self, activate_session_mock, stderr, stdout): + args = ['--help'] + options = mock.MagicMock() + + # Mock out the xmlrpc server + session = mock.MagicMock() + + # Run it and check immediate output + # args: --help + # expected: failed, help info shows + with self.assertRaises(SystemExit) as cm: + handle_edit_user(options, session, args) + actual_stdout = stdout.getvalue() + actual_stderr = stderr.getvalue() + expected_stdout = """Usage: %s edit-user name [options] +(Specify the --help global option for a list of other help options) + +Options: + -h, --help show this help message and exit + --rename=RENAME Rename the user + --krb=KRB Change kerberos principal of the user +""" % progname + expected_stderr = '' + self.assertMultiLineEqual(actual_stdout, expected_stdout) + self.assertMultiLineEqual(actual_stderr, expected_stderr) + # Finally, assert that things were called as we expected. + activate_session_mock.assert_not_called() + session.editUser.assert_not_called() + self.assertEqual(cm.exception.code, 0) + + @mock.patch('sys.stdout', new_callable=six.StringIO) + @mock.patch('sys.stderr', new_callable=six.StringIO) + @mock.patch('koji_cli.commands.activate_session') + def test_handle_edit_user_no_arg(self, activate_session_mock, stderr, stdout): + args = [] + options = mock.MagicMock() + + # Mock out the xmlrpc server + session = mock.MagicMock() + + # Run it and check immediate output + # args: --help + # expected: failed, help info shows + with self.assertRaises(SystemExit) as cm: + handle_edit_user(options, session, args) + actual_stdout = stdout.getvalue() + actual_stderr = stderr.getvalue() + expected_stdout = '' + expected_stderr = """Usage: %(progname)s edit-user name [options] +(Specify the --help global option for a list of other help options) + +%(progname)s: error: You must specify the username of the user to edit +""" % {'progname': progname} + self.assertMultiLineEqual(actual_stdout, expected_stdout) + self.assertMultiLineEqual(actual_stderr, expected_stderr) + # Finally, assert that things were called as we expected. + activate_session_mock.assert_not_called() + session.editUser.assert_not_called() + self.assertEqual(cm.exception.code, 2) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_hub/test_edit_user.py b/tests/test_hub/test_edit_user.py new file mode 100644 index 00000000..c2dba357 --- /dev/null +++ b/tests/test_hub/test_edit_user.py @@ -0,0 +1,60 @@ +import mock +import unittest + +import koji +import kojihub + +UP = kojihub.UpdateProcessor + +class TestEditUser(unittest.TestCase): + + def getUpdate(self, *args, **kwargs): + update = UP(*args, **kwargs) + update.execute = mock.MagicMock() + self.updates.append(update) + return update + + def setUp(self): + self.updates = [] + self._singleValue = mock.patch('kojihub._singleValue').start() + self.get_user = mock.patch('kojihub.get_user').start() + self.context = mock.patch('kojihub.context').start() + self.UpdateProcessor = mock.patch('kojihub.UpdateProcessor', + side_effect=self.getUpdate).start() + # It seems MagicMock will not automatically handle attributes that + # start with "assert" + self.context.session.assertLogin = mock.MagicMock() + + def tearDown(self): + mock.patch.stopall() + + def test_edit(self): + self.get_user.return_value = {'id': 333, + 'name': 'user', + 'krb_principal': 'krb'} + self._singleValue.return_value = None + + kojihub._edit_user('user', name='newuser', krb_principal='krb') + # check the update + self.assertEqual(len(self.updates), 1) + update = self.updates[0] + self.assertEqual(update.table, 'users') + self.assertEqual(update.data, {'name': 'newuser'}) + self.assertEqual(update.values, {'userID': 333}) + self.assertEqual(update.clauses, ['id = %(userID)i']) + + kojihub._edit_user('user', name='user', krb_principal='newkrb') + # check the insert/update + self.assertEqual(len(self.updates), 2) + update = self.updates[1] + self.assertEqual(update.table, 'users') + self.assertEqual(update.data, {'krb_principal': 'newkrb'}) + self.assertEqual(update.values, {'userID': 333}) + self.assertEqual(update.clauses, ['id = %(userID)i']) + + self._singleValue.reset_mock() + self._singleValue.return_value = 2 + with self.assertRaises(koji.GenericError) as cm: + kojihub._edit_user('user', name='newuser') + self._singleValue.assert_called_once() + self.assertEqual(cm.exception.args[0], 'Name newuser already taken by user 2')