From: Markus Lehtonen Date: Wed, 4 Dec 2013 08:00:11 +0000 (+0200) Subject: Support function calls with demoted privileges X-Git-Tag: submit/devel/20190730.075437~70 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=42eb7be329afd9bceb34ac580959c38c906ff0d2;p=services%2Fobs-service-git-buildpackage.git Support function calls with demoted privileges Implement base functionality to do function calls with different UID/GID. This is done by spawning a child process, then changing UID/GID and doing the function call there. The functionality is added in a new python module, in a new RPM subpackage - in order to be easily utilize this in the GBS source service, too. Change-Id: I1ac345501d692fa11fdb83d3d8ea563780bafcbb Signed-off-by: Markus Lehtonen --- diff --git a/.coveragerc b/.coveragerc index 9930203..921c9ef 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,2 +1,2 @@ [run] -include = obs_service_gbp/*, gbp_repocache/* +include = obs_service_gbp/*, obs_service_gbp_utils/*, gbp_repocache/* diff --git a/obs_service_gbp_utils/__init__.py b/obs_service_gbp_utils/__init__.py new file mode 100644 index 0000000..ed0aeb6 --- /dev/null +++ b/obs_service_gbp_utils/__init__.py @@ -0,0 +1,95 @@ +# vim:fileencoding=utf-8:et:ts=4:sw=4:sts=4 +# +# Copyright (C) 2013 Intel Corporation +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +"""Helper functionality for the GBP OBS source service""" + +import os +import grp +import pwd +import sys +from multiprocessing import Process, Queue + + +_RET_FORK_OK = 0 +_RET_FORK_ERR = 1 + + +class GbpServiceError(Exception): + """General error class for the source service""" + pass + +def _demoted_child_call(uid, gid, ret_data_q, func, args, kwargs): + """Call a function/method with different uid/gid""" + # Set UID and GID + try: + if uid and uid > 0: + os.setresuid(uid, uid, uid) + if gid and gid > 0: + os.setresgid(gid, gid, gid) + except OSError as err: + ret_data_q.put(GbpServiceError("Setting UID/GID (%s:%s) failed: %s" % + (uid, gid, err))) + sys.exit(_RET_FORK_ERR) + # Call the function + try: + ret = func(*args, **kwargs) + except Exception as err: + ret_data_q.put(err) + sys.exit(_RET_FORK_ERR) + else: + ret_data_q.put(ret) + sys.exit(_RET_FORK_OK) + +def sanitize_uid_gid(user, group): + """Get numerical uid and gid""" + # Get numerical uid and gid + uid = -1 + gid = -1 + try: + if user: + try: + uid = int(user) + except ValueError: + uid = pwd.getpwnam(user).pw_uid + if group: + try: + gid = int(group) + except ValueError: + gid = grp.getgrnam(group).gr_gid + except KeyError as err: + raise GbpServiceError('Unable to find UID/GID: %s' % err) + return (uid, gid) + +def fork_call(user, group, func, *args, **kwargs): + """Fork and call a function. The function should return an integer""" + # Get numerical uid and gid + uid, gid = sanitize_uid_gid(user, group) + + # Run function in a child process + data_q = Queue() + child = Process(target=_demoted_child_call, args=(uid, gid, data_q, func, + args, kwargs)) + child.start() + child.join() + ret_data = data_q.get() + ret_code = child.exitcode + if ret_code == _RET_FORK_OK: + return ret_data + else: + raise ret_data + diff --git a/packaging/obs-service-git-buildpackage.spec b/packaging/obs-service-git-buildpackage.spec index a8ce35a..0937217 100644 --- a/packaging/obs-service-git-buildpackage.spec +++ b/packaging/obs-service-git-buildpackage.spec @@ -32,6 +32,16 @@ It supports cloning/updating repo from git and exporting sources and packaging files that are managed with git-buildpackage tools. +%package utils +Summary: Utility fuctions for the GBP OBS source service +Group: Development/Tools/Building +Requires: python >= 2.6 + +%description utils +This package contains generic utility functions for the git-buildpackage OBS +source service. + + %package -n gbp-repocache Summary: Git repository cache API Group: Development/Tools/Building @@ -74,6 +84,10 @@ rm -rf %{buildroot}%{python_sitelib}/*info %dir %{_sysconfdir}/obs/services %config %{_sysconfdir}/obs/services/* +%files utils +%doc COPYING +%{python_sitelib}/obs_service_gbp_utils + %files -n gbp-repocache %doc COPYING %{python_sitelib}/gbp_repocache diff --git a/setup.cfg b/setup.cfg index d6883e6..0e06169 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [nosetests] with-coverage=1 -cover-package=obs_service_gbp,gbp_repocache +cover-package=obs_service_gbp,obs_service_gbp_utils,gbp_repocache with-xunit=1 with-doctest=1 diff --git a/setup.py b/setup.py index dff1621..10c192a 100644 --- a/setup.py +++ b/setup.py @@ -35,7 +35,7 @@ setup(name='obs_service_gbp', description='OBS source service utilizing git-buildpackage', author='Markus Lehtonen', author_email='markus.lehtonen@linux.intel.com', - packages=['obs_service_gbp', 'gbp_repocache'], + packages=['obs_service_gbp', 'obs_service_gbp_utils', 'gbp_repocache'], data_files=[('/usr/lib/obs/service', ['service/git-buildpackage', 'service/git-buildpackage.service']), ('/etc/obs/services', ['config/git-buildpackage'])], diff --git a/tests/test_obs_service_gbp_utils.py b/tests/test_obs_service_gbp_utils.py new file mode 100644 index 0000000..2bbc486 --- /dev/null +++ b/tests/test_obs_service_gbp_utils.py @@ -0,0 +1,111 @@ +# vim:fileencoding=utf-8:et:ts=4:sw=4:sts=4 +# +# Copyright (C) 2013 Intel Corporation +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +"""Tests for the GBP OBS service helper functionality""" + +import grp +import pwd +import os +from nose.tools import assert_raises, eq_, ok_ # pylint: disable=E0611 +from multiprocessing import Queue + +from obs_service_gbp_utils import (fork_call, _demoted_child_call, + GbpServiceError, _RET_FORK_OK) + + +class _DummyException(Exception): + """Dummy exception for testing""" + pass + +class TestForkCall(object): + """Test the functionality for calling functions in a child thread""" + + def __init__(self): + self._uid = os.getuid() + self._gid = os.getgid() + self._name = pwd.getpwuid(self._uid).pw_name + self._group = grp.getgrgid(self._gid).gr_name + + @staticmethod + def _no_fork_call(uid, gid, func, *args, **kwargs): + """For testing demoted call without actually forking""" + data_q = Queue() + try: + _demoted_child_call(uid, gid, data_q, func, args, kwargs) + except SystemExit as err: + ret_code = err.code + ret_data = data_q.get() + if ret_code == _RET_FORK_OK: + return ret_data + else: + raise ret_data + + @staticmethod + def _dummy_ok(): + """Helper method returning 'ok'""" + return 'ok' + + @staticmethod + def _dummy_raise(): + """Helper method raising an exception""" + raise _DummyException('Dummy error') + + @staticmethod + def _dummy_args(arg1, arg2, arg3): + """Helper method returning all its args""" + return (arg1, arg2, arg3) + + def test_success(self): + """Basic test for successful call""" + eq_(fork_call(None, None, self._dummy_ok), 'ok') + + eq_(fork_call(None, None, self._dummy_args, 1, '2', arg3='foo'), + (1, '2', 'foo')) + + ok_(os.getpid() != fork_call(None, None, os.getpid)) + + def test_fail(self): + """Tests for function call failures""" + with assert_raises(_DummyException): + fork_call(None, None, self._dummy_raise) + + with assert_raises(TypeError): + fork_call(None, None, self._dummy_ok, 'unexptected_arg') + + def test_demoted_call_no(self): + """Test running with different UID/GID""" + eq_(fork_call(self._name, self._group, self._dummy_ok), 'ok') + eq_(fork_call(self._uid, None, self._dummy_ok), 'ok') + eq_(fork_call(None, self._gid, self._dummy_ok), 'ok') + + eq_(self._no_fork_call(self._uid, self._gid, self._dummy_ok), 'ok') + + def test_demoted_call_fail(self): + """Test running with invalid UID/GID""" + with assert_raises(GbpServiceError): + fork_call('_non_existent_user', None, self._dummy_ok) + with assert_raises(GbpServiceError): + fork_call(None, '_non_existen_group', self._dummy_ok) + + with assert_raises(GbpServiceError): + self._no_fork_call(99999, None, self._dummy_ok) + with assert_raises(GbpServiceError): + self._no_fork_call(None, 99999, self._dummy_ok) + with assert_raises(_DummyException): + self._no_fork_call(self._uid, self._gid, self._dummy_raise) +