From 47c4b5d70baaea36dbf637bee907a33229cf7a60 Mon Sep 17 00:00:00 2001 From: Yu Ming Zhu Date: Fri, 16 Jul 2021 15:33:39 +0000 Subject: [PATCH 1/7] kojid: extend SCM.assert_allowed with hub policy This is a simple extention of `SCM.assert_allowed` - `assert_allowed_by_policy` will set the default "use_common" to False which is different to the old behavior - `channel`, `user_id`, `scratch` are passed in the `policy_data` with scminfo right now. This is a prototype for this change, and there are some other solutions could be implemented too - Use a scmpolicy plugin as `postSCMCheckout` callback, the pro is that we can do more checks after the source is initialized on builder, meanwhile, the con is that the source will be downloaded even it is denied by policy. It might be a potential risk? - Do the scm check in hub's `make_task`, this looks straightforward, but may lack some builder's information fixes: #2757 --- builder/kojid | 93 ++++++++++++++++--- builder/kojid.conf | 8 ++ hub/kojixmlrpc.py | 7 ++ koji/daemon.py | 79 +++++++++++++++- koji/tasks.py | 18 ++-- tests/test_scm.py | 226 ++++++++++++++++++++++++++++++++++++++++++++- vm/kojivmd | 17 +++- vm/kojivmd.conf | 8 ++ 8 files changed, 431 insertions(+), 25 deletions(-) diff --git a/builder/kojid b/builder/kojid index ecf8451f..3dfc5009 100755 --- a/builder/kojid +++ b/builder/kojid @@ -1633,7 +1633,17 @@ class BuildMavenTask(BaseBuildTask): self.opts = opts scm = SCM(url) - scm.assert_allowed(self.options.allowed_scms) + scm_policy_opts = { + 'user_id': self.taskinfo['owner'], + 'channel': self.session.getChannel(self.taskinfo['channel_id'], + strict=True)['name'], + 'scratch': self.opts.get('scratch') + } + scm.assert_allowed(allowed=self.options.allowed_scms, + session=self.session, + by_config=self.options.allowed_scms_use_config, + by_policy=self.options.allowed_scms_use_policy, + opts=scm_policy_opts) repo_id = opts.get('repo_id') if not repo_id: raise koji.BuildError('A repo_id must be provided') @@ -1707,7 +1717,11 @@ class BuildMavenTask(BaseBuildTask): if self.opts.get('patches'): patchlog = self.workdir + '/patches.log' patch_scm = SCM(self.opts.get('patches')) - patch_scm.assert_allowed(self.options.allowed_scms) + patch_scm.assert_allowed(allowed=self.options.allowed_scms, + session=self.session, + by_config=self.options.allowed_scms_use_config, + by_policy=self.options.allowed_scms_use_policy, + opts=scm_policy_opts) self.run_callbacks('preSCMCheckout', scminfo=patch_scm.get_info(), build_tag=build_tag, scratch=opts.get('scratch'), buildroot=buildroot) @@ -1990,7 +2004,16 @@ class WrapperRPMTask(BaseBuildTask): assert False # pragma: no cover scm = SCM(spec_url) - scm.assert_allowed(self.options.allowed_scms) + scm.assert_allowed(allowed=self.options.allowed_scms, + session=self.session, + by_config=self.options.allowed_scms_use_config, + by_policy=self.options.allowed_scms_use_policy, + opts={ + 'user_id': self.taskinfo['owner'], + 'channel': self.session.getChannel(self.taskinfo['channel_id'], + strict=True)['name'], + 'scratch': opts.get('scratch') + }) repo_id = opts.get('repo_id') if not repo_id: @@ -2979,7 +3002,16 @@ class ImageTask(BaseTaskHandler): self.logger.debug("ksfile = %s" % ksfile) if self.opts.get('ksurl'): scm = SCM(self.opts['ksurl']) - scm.assert_allowed(self.options.allowed_scms) + scm.assert_allowed(allowed=self.options.allowed_scms, + session=self.session, + by_config=self.options.allowed_scms_use_config, + by_policy=self.options.allowed_scms_use_policy, + opts={ + 'user_id': self.taskinfo['owner'], + 'channel': self.session.getChannel(self.taskinfo['channel_id'], + strict=True)['name'], + 'scratch': self.opts.get('scratch') + }) logfile = os.path.join(self.workdir, 'checkout.log') self.run_callbacks('preSCMCheckout', scminfo=scm.get_info(), build_tag=build_tag, scratch=self.opts.get('scratch'), @@ -3453,7 +3485,16 @@ class LiveMediaTask(ImageTask): can find the checked out templates. """ scm = SCM(self.opts['lorax_url']) - scm.assert_allowed(self.options.allowed_scms) + scm.assert_allowed(allowed=self.options.allowed_scms, + session=self.session, + by_config=self.options.allowed_scms_use_config, + by_policy=self.options.allowed_scms_use_policy, + opts={ + 'user_id': self.taskinfo['owner'], + 'channel': self.session.getChannel(self.taskinfo['channel_id'], + strict=True)['name'], + 'scratch': self.opts.get('scratch') + }) logfile = os.path.join(self.workdir, 'lorax-templates-checkout.log') checkout_dir = scm.checkout(build_root.tmpdir(), self.session, self.getUploadDir(), logfile) @@ -3700,7 +3741,16 @@ class OzImageTask(BaseTaskHandler): self.logger.debug("ksfile = %s" % ksfile) if self.opts.get('ksurl'): scm = SCM(self.opts['ksurl']) - scm.assert_allowed(self.options.allowed_scms) + scm.assert_allowed(allowed=self.options.allowed_scms, + session=self.session, + by_config=self.options.allowed_scms_use_config, + by_policy=self.options.allowed_scms_use_policy, + opts={ + 'user_id': self.taskinfo['owner'], + 'channel': self.session.getChannel(self.taskinfo['channel_id'], + strict=True)['name'], + 'scratch': self.opts.get('scratch') + }) logfile = os.path.join(self.workdir, 'checkout-%s.log' % self.arch) self.run_callbacks('preSCMCheckout', scminfo=scm.get_info(), build_tag=build_tag, scratch=self.opts.get('scratch')) @@ -4527,7 +4577,16 @@ class BuildIndirectionImageTask(OzImageTask): self.logger.debug("filepath = %s" % filepath) if fileurl: scm = SCM(fileurl) - scm.assert_allowed(self.options.allowed_scms) + scm.assert_allowed(allowed=self.options.allowed_scms, + session=self.session, + by_config=self.options.allowed_scms_use_config, + by_policy=self.options.allowed_scms_use_policy, + opts={ + 'user_id': self.taskinfo['owner'], + 'channel': self.session.getChannel(self.taskinfo['channel_id'], + strict=True)['name'], + 'scratch': self.opts.get('scratch') + }) self.run_callbacks('preSCMCheckout', scminfo=scm.get_info(), build_tag=build_tag, scratch=self.opts.get('scratch')) logfile = os.path.join(self.workdir, 'checkout.log') @@ -4935,12 +4994,20 @@ class BuildSRPMFromSCMTask(BaseBuildTask): return self.checkHostArch(tag, hostdata) def handler(self, url, build_tag, opts=None): - # will throw a BuildError if the url is invalid - scm = SCM(url) - scm.assert_allowed(self.options.allowed_scms) - if opts is None: opts = {} + # will throw a BuildError if the url is invalid + scm = SCM(url) + scm.assert_allowed(allowed=self.options.allowed_scms, + session=self.session, + by_config=self.options.allowed_scms_use_config, + by_policy=self.options.allowed_scms_use_policy, + opts={ + 'user_id': self.taskinfo['owner'], + 'channel': self.session.getChannel(self.taskinfo['channel_id'], + strict=True)['name'], + 'scratch': opts.get('scratch') + }) repo_id = opts.get('repo_id') if not repo_id: raise koji.BuildError("A repo id must be provided") @@ -6362,6 +6429,8 @@ def get_options(): 'mock_bootstrap_image': False, 'pkgurl': None, 'allowed_scms': '', + 'allowed_scms_by_config': True, + 'allowed_scms_by_policy': False, 'scm_credentials_dir': None, 'support_rpm_source_layout': True, 'yum_proxy': None, @@ -6390,7 +6459,7 @@ def get_options(): elif name in ['offline_retry', 'use_createrepo_c', 'createrepo_skip_stat', 'createrepo_update', 'use_fast_upload', 'support_rpm_source_layout', 'build_arch_can_fail', 'no_ssl_verify', 'log_timestamps', - 'allow_noverifyssl']: + 'allow_noverifyssl', 'allowed_scms_by_config', 'allowed_scms_by_policy']: defaults[name] = config.getboolean('kojid', name) elif name in ['plugin', 'plugins']: defaults['plugin'] = value.split() diff --git a/builder/kojid.conf b/builder/kojid.conf index 4dc2a87e..777a421f 100644 --- a/builder/kojid.conf +++ b/builder/kojid.conf @@ -77,6 +77,14 @@ topurl=http://hub.example.com/kojifiles ; is run by default. allowed_scms=scm.example.com:/cvs/example git.example.org:/example svn.example.org:/users/*:no +; If use the option allowed_scms above for allowing / denying SCM, default: true +; allowed_scms_use_config = true + +; If use hub policy build_from_scm for allowing / denying SCM, default: false +; notice that if both options are enabled, both assertions will be applied, and user_common and +; source_cmd will be overridden by the policy's result. +; allowed_scms_use_policy = false + ; A directory to bind mount into Source RPM creation so that some ; credentials can be supplied when required to fetch sources, e.g. ; when the place the sources are fetched from requires all accesses to diff --git a/hub/kojixmlrpc.py b/hub/kojixmlrpc.py index 468461be..196e4730 100644 --- a/hub/kojixmlrpc.py +++ b/hub/kojixmlrpc.py @@ -524,6 +524,13 @@ _default_policies = { has_perm admin :: allow all :: deny ''', + 'build_from_scm': ''' + has_perm admin :: allow + # match scmtype CVS CVS+SSH && match scmhost scm.example.com && match scmrepository /cvs/example :: allow + # match scmtype GIT GIT+SSH && match scmhost git.example.org && match scmrepository /example :: allow + # match scmtype SVN SVN+SSH && match scmhost svn.example.org && match scmrepository /users/* :: allow + all :: deny + ''', # noqa: E501 'package_list': ''' has_perm admin :: allow has_perm tag :: allow diff --git a/koji/daemon.py b/koji/daemon.py index bcd01b21..3eba1a7e 100644 --- a/koji/daemon.py +++ b/koji/daemon.py @@ -327,7 +327,16 @@ class SCM(object): # return parsed values return (scheme, user, netloc, path, query, fragment) - def assert_allowed(self, allowed): + def assert_allowed(self, allowed='', session=None, by_config=True, by_policy=False, opts=None): + if by_config: + self.assert_allowed_by_config(allowed or '') + if by_policy: + if session is None: + raise koji.ConfigurationError( + 'When allowed SCM assertion is by policy, session must be passed in.') + self.assert_allowed_by_policy(session, **(opts or {})) + + def assert_allowed_by_config(self, allowed): """ Check this scm against allowed list and apply options @@ -396,6 +405,74 @@ class SCM(object): raise koji.BuildError( '%s:%s is not in the list of allowed SCMs' % (self.host, self.repository)) + def assert_allowed_by_policy(self, session, **opts): + """ + Check this scm against hub policy: build_from_scm and apply options + + The policy data is the combination of scminfo with scm prefix and kwargs. + It should at least contain following keys: + + - scmurl + - scmscheme + - scmuser + - scmhost + - scmrepository + - scmmodule + - scmrevision + - scmtype + + More keys could be added in opts. You can pass any reasonable data which could be handled + by policy tests, like: + + - scratch (if the task is scratch) + - channel (which channel the task is assigned) + - user_id (the task owner) + + The format of the action returned from build_from_scm could be one of following forms:: + + allow [use_common] [source_cmd] + deny [reason] + + If use_common is not set, use_common property is False. + If source_cmd is none, it will be parsed as None. If it not set, the default value: + ['make', 'sources'], or the value set by :func:`~koji.daemon.SCM.assert_allowed_by_config` + will be set. + + Policy example: + + build_from_scm = + bool scratch :: allow none + match scmhost scm.example.com :: allow use_common make sources + match scmhost scm2.example.com :: allow + all :: deny + + + :param koji.ClientSession session: the session object to call hub xmlrpc APIs. + It should be a host session. + + :raises koji.BuildError: if the scm is denied. + """ + policy_data = {} + for k, v in six.iteritems(self.get_info()): + if not k.startswith('scm'): + k = 'scm' + k + policy_data[k] = v + policy_data.update(opts) + result = (session.host.evalPolicy('build_from_scm', policy_data) or '').split() + is_allowed = result and result[0].lower() in ('yes', 'true', 'allow', 'allowed') + if not is_allowed: + raise koji.BuildError( + 'SCM: %s:%s is not allowed, reason: %s' % (self.host, self.repository, + ' '.join(result[1:]) or None)) + # Apply options when it's allowed + applied = result[1:] + self.use_common = len(applied) != 0 and applied[0].lower() == 'use_common' + idx = 1 if self.use_common else 0 + self.source_cmd = applied[idx:] or self.source_cmd + if self.source_cmd is not None and len(self.source_cmd) > 0 \ + and self.source_cmd[0].lower() == 'none': + self.source_cmd = None + def checkout(self, scmdir, session=None, uploadpath=None, logfile=None): """ Checkout the module from SCM. Accepts the following parameters: diff --git a/koji/tasks.py b/koji/tasks.py index ca741caa..0a03c725 100644 --- a/koji/tasks.py +++ b/koji/tasks.py @@ -312,6 +312,7 @@ class BaseTaskHandler(object): self.workdir = workdir self.logger = logging.getLogger("koji.build.BaseTaskHandler") self.manager = None + self.taskinfo = None def setManager(self, manager): """Set the manager attribute @@ -597,15 +598,20 @@ class BaseTaskHandler(object): def run_callbacks(self, plugin, *args, **kwargs): if 'taskinfo' not in kwargs: - try: - taskinfo = self.taskinfo - except AttributeError: - self.taskinfo = self.session.getTaskInfo(self.id, request=True) - taskinfo = self.taskinfo - kwargs['taskinfo'] = taskinfo + kwargs['taskinfo'] = self.taskinfo kwargs['session'] = self.session koji.plugin.run_callbacks(plugin, *args, **kwargs) + @property + def taskinfo(self): + if not getattr(self, '_taskinfo', None): + self._taskinfo = self.session.getTaskInfo(self.id, request=True, strict=True) + return self._taskinfo + + @taskinfo.setter + def taskinfo(self, taskinfo): + self._taskinfo = taskinfo + class FakeTask(BaseTaskHandler): Methods = ['someMethod'] diff --git a/tests/test_scm.py b/tests/test_scm.py index 54b26910..2ce67b46 100644 --- a/tests/test_scm.py +++ b/tests/test_scm.py @@ -8,9 +8,40 @@ import unittest import koji import koji.daemon +import koji.policy from koji.daemon import SCM +policy = { + 'one': ''' + match scmhost goodserver :: allow none + match scmhost badserver :: deny + match scmhost maybeserver && match scmrepository /badpath/* :: deny + all :: allow + ''', + 'two': ''' + match scmhost default :: allow + match scmhost nocommon :: allow + match scmhost common :: allow use_common + match scmhost srccmd :: allow fedpkg sources + match scmhost nosrc :: allow none + match scmhost mixed && match scmrepository /foo/* :: allow + match scmhost mixed && match scmrepository /bar/* :: allow use_common + match scmhost mixed && match scmrepository /baz/* :: allow fedpkg sources + match scmhost mixed && match scmrepository /foobar/* :: allow use_common fedpkg sources + match scmhost mixed && match scmrepository /foobaz/* :: allow use_common none + ''' +} + +class FakePolicy(object): + + def __init__(self, rule): + base_tests = koji.policy.findSimpleTests(vars(koji.policy)) + self.ruleset = koji.policy.SimpleRuleSet(rule.splitlines(), base_tests) + + def evalPolicy(self, name, data): + return self.ruleset.apply(data) + class TestSCM(unittest.TestCase): @@ -67,8 +98,22 @@ class TestSCM(unittest.TestCase): self.assertEqual(scm.source_cmd, ['make', 'sources']) self.assertEqual(scm.scmtype, 'GIT') + def test_assert_allowed_basic(self): + scm = SCM("git://scm.example.com/path1#1234") + + # session must be passed + with self.assertRaises(koji.GenericError) as cm: + scm.assert_allowed(session=None, by_config=False, by_policy=True) + self.assertEqual(str(cm.exception), + 'When allowed SCM assertion is by policy, session must be passed in.') + + # allowed could not be None + scm.assert_allowed_by_config = mock.MagicMock() + scm.assert_allowed(allowed=None, by_config=True, by_policy=False) + scm.assert_allowed_by_config.assert_called_once_with('') + @mock.patch('logging.getLogger') - def test_allowed(self, getLogger): + def test_allowed_by_config(self, getLogger): config = ''' goodserver:*:no !badserver:* @@ -104,7 +149,7 @@ class TestSCM(unittest.TestCase): raise AssertionError("allowed bad url: %s" % url) @mock.patch('logging.getLogger') - def test_badrule(self, getLogger): + def test_badrule_by_config(self, getLogger): config = ''' bogus-entry-should-be-ignored goodserver:*:no @@ -115,7 +160,7 @@ class TestSCM(unittest.TestCase): scm.assert_allowed(config) @mock.patch('logging.getLogger') - def test_opts(self, getLogger): + def test_opts_by_config(self, getLogger): config = ''' default:* nocommon:*:no @@ -196,6 +241,181 @@ class TestSCM(unittest.TestCase): with self.assertRaises(koji.BuildError): scm.assert_allowed(config) + def test_allowed_by_policy(self): + good = [ + "git://goodserver/path1#1234", + "git+ssh://maybeserver/path1#1234", + ] + bad = [ + "cvs://badserver/projects/42#ref", + "svn://badserver/projects/42#ref", + "git://maybeserver/badpath/project#1234", + "git://maybeserver//badpath/project#1234", + "git://maybeserver////badpath/project#1234", + "git://maybeserver/./badpath/project#1234", + "git://maybeserver//.//badpath/project#1234", + "git://maybeserver/goodpath/../badpath/project#1234", + "git://maybeserver/goodpath/..//badpath/project#1234", + "git://maybeserver/..//badpath/project#1234", + ] + session = mock.MagicMock() + session.host.evalPolicy.side_effect = FakePolicy(policy['one']).evalPolicy + for url in good: + scm = SCM(url) + scm.assert_allowed(session=session, by_config=False, by_policy=True) + for url in bad: + scm = SCM(url) + with self.assertRaises(koji.BuildError) as cm: + scm.assert_allowed(session=session, by_config=False, by_policy=True) + self.assertRegexpMatches(str(cm.exception), '^SCM: .* is not allowed, reason: None$') + + def test_opts_by_policy(self): + session = mock.MagicMock() + session.host.evalPolicy.side_effect = FakePolicy(policy['two']).evalPolicy + + url = "git://default/koji.git#1234" + scm = SCM(url) + scm.assert_allowed_by_policy(session=session) + self.assertEqual(scm.use_common, False) + self.assertEqual(scm.source_cmd, ['make', 'sources']) + + url = "git://nocommon/koji.git#1234" + scm = SCM(url) + scm.assert_allowed_by_policy(session=session) + self.assertEqual(scm.use_common, False) + self.assertEqual(scm.source_cmd, ['make', 'sources']) + + url = "git://common/koji.git#1234" + scm = SCM(url) + scm.assert_allowed_by_policy(session=session) + self.assertEqual(scm.use_common, True) + self.assertEqual(scm.source_cmd, ['make', 'sources']) + + url = "git://srccmd/koji.git#1234" + scm = SCM(url) + scm.assert_allowed_by_policy(session=session) + self.assertEqual(scm.use_common, False) + self.assertEqual(scm.source_cmd, ['fedpkg', 'sources']) + + url = "git://nosrc/koji.git#1234" + scm = SCM(url) + scm.assert_allowed_by_policy(session=session) + self.assertEqual(scm.use_common, False) + self.assertEqual(scm.source_cmd, None) + + url = "git://mixed/foo/koji.git#1234" + scm = SCM(url) + scm.assert_allowed_by_policy(session=session) + self.assertEqual(scm.use_common, False) + self.assertEqual(scm.source_cmd, ['make', 'sources']) + + url = "git://mixed/bar/koji.git#1234" + scm = SCM(url) + scm.assert_allowed_by_policy(session=session) + self.assertEqual(scm.use_common, True) + self.assertEqual(scm.source_cmd, ['make', 'sources']) + + url = "git://mixed/baz/koji.git#1234" + scm = SCM(url) + scm.assert_allowed_by_policy(session=session) + self.assertEqual(scm.use_common, False) + self.assertEqual(scm.source_cmd, ['fedpkg', 'sources']) + + url = "git://mixed/koji.git#1234" + scm = SCM(url) + with self.assertRaises(koji.BuildError): + scm.assert_allowed_by_policy(session=session) + + url = "git://mixed/foo/koji.git#1234" + scm = SCM(url) + scm.assert_allowed_by_policy(session=session) + self.assertEqual(scm.use_common, False) + self.assertEqual(scm.source_cmd, ['make', 'sources']) + + url = "git://mixed/bar/koji.git#1234" + scm = SCM(url) + scm.assert_allowed_by_policy(session=session) + self.assertEqual(scm.use_common, True) + self.assertEqual(scm.source_cmd, ['make', 'sources']) + + url = "git://mixed/baz/koji.git#1234" + scm = SCM(url) + scm.assert_allowed_by_policy(session=session) + self.assertEqual(scm.use_common, False) + self.assertEqual(scm.source_cmd, ['fedpkg', 'sources']) + + url = "git://mixed/foobar/koji.git#1234" + scm = SCM(url) + scm.assert_allowed_by_policy(session=session) + self.assertEqual(scm.use_common, True) + self.assertEqual(scm.source_cmd, ['fedpkg', 'sources']) + + url = "git://mixed/foobaz/koji.git#1234" + scm = SCM(url) + scm.assert_allowed_by_policy(session=session) + self.assertEqual(scm.use_common, True) + self.assertIsNone(scm.source_cmd) + + url = "git://nomatch/koji.git#1234" + scm = SCM(url) + with self.assertRaises(koji.BuildError): + scm.assert_allowed_by_policy(session=session) + + def test_assert_allowed_by_both(self): + config = ''' + default:*:no: + mixed:/foo/*:yes + mixed:/bar/*:no + mixed:/baz/*:no:centpkg,sources + mixed:/foobar/*:no: + mixed:/foobaz/*:no:centpkg,sources + ''' + + session = mock.MagicMock() + session.host.evalPolicy.side_effect = FakePolicy(policy['two']).evalPolicy + + url = "git://default/koji.git#1234" + scm = SCM(url) + # match scmhost default :: allow + scm.assert_allowed(allowed=config, session=session, by_config=True, by_policy=True) + self.assertEqual(scm.use_common, False) + self.assertIsNone(scm.source_cmd) + + url = "git://mixed/foo/koji.git#1234" + scm = SCM(url) + # match scmhost mixed && match scmrepository /foo/* :: allow + scm.assert_allowed(allowed=config, session=session, by_config=True, by_policy=True) + self.assertEqual(scm.use_common, False) + self.assertEqual(scm.source_cmd, ['make', 'sources']) + + url = "git://mixed/bar/koji.git#1234" + scm = SCM(url) + # match scmhost mixed && match scmrepository /bar/* :: allow use_common + scm.assert_allowed(allowed=config, session=session, by_config=True, by_policy=True) + self.assertEqual(scm.use_common, True) + self.assertEqual(scm.source_cmd, ['make', 'sources']) + + url = "git://mixed/baz/koji.git#1234" + scm = SCM(url) + # match scmhost mixed && match scmrepository /baz/* :: allow fedpkg sources + scm.assert_allowed(allowed=config, session=session, by_config=True, by_policy=True) + self.assertEqual(scm.use_common, False) + self.assertEqual(scm.source_cmd, ['fedpkg', 'sources']) + + url = "git://mixed/foobar/koji.git#1234" + scm = SCM(url) + # match scmhost mixed && match scmrepository /foobar/* :: allow use_common fedpkg sources + scm.assert_allowed(allowed=config, session=session, by_config=True, by_policy=True) + self.assertEqual(scm.use_common, True) + self.assertEqual(scm.source_cmd, ['fedpkg', 'sources']) + + url = "git://mixed/foobaz/koji.git#1234" + scm = SCM(url) + # match scmhost mixed && match scmrepository /foobaz/* :: allow use_common none + scm.assert_allowed(allowed=config, session=session, by_config=True, by_policy=True) + self.assertEqual(scm.use_common, True) + self.assertIsNone(scm.source_cmd) + class TestSCMCheckouts(unittest.TestCase): diff --git a/vm/kojivmd b/vm/kojivmd index 89e96cf0..75ab017d 100755 --- a/vm/kojivmd +++ b/vm/kojivmd @@ -138,6 +138,8 @@ def get_options(): 'offline_retry': True, 'offline_retry_interval': 120, 'allowed_scms': '', + 'allowed_scms_by_config': True, + 'allowed_scms_by_policy': False, 'cert': None, 'serverca': None} if config.has_section('kojivmd'): @@ -149,7 +151,8 @@ def get_options(): defaults[name] = int(value) except ValueError: quit("value for %s option must be a valid integer" % name) - elif name in ['offline_retry', 'no_ssl_verify']: + elif name in ['offline_retry', 'no_ssl_verify', 'allowed_scms_by_config', + 'allowed_scms_by_policy']: defaults[name] = config.getboolean('kojivmd', name) elif name in ['plugin', 'plugins']: defaults['plugin'] = value.split() @@ -325,8 +328,16 @@ class WinBuildTask(MultiPlatformTask): # verify the urls before passing them to the VM for url in [source_url] + koji.util.to_list(subopts.values()): scm = SCM(url) - scm.assert_allowed(self.options.allowed_scms) - + scm.assert_allowed(allowed=self.options.allowed_scms, + session=self.session, + by_config=self.options.allowed_scms_use_config, + by_policy=self.options.allowed_scms_use_policy, + opts={ + 'user_id': self.taskinfo['owner'], + 'channel': self.session.getChannel(self.taskinfo['channel_id'], + strict=True)['name'], + 'scratch': opts.get('scratch') + }) task_info = self.session.getTaskInfo(self.id) target_info = self.session.getBuildTarget(target) if not target_info: diff --git a/vm/kojivmd.conf b/vm/kojivmd.conf index 78e3f249..e6ae14fd 100644 --- a/vm/kojivmd.conf +++ b/vm/kojivmd.conf @@ -27,6 +27,14 @@ server=http://hub.example.com/kojihub ; dir, and will raise an exception if it cannot. allowed_scms=scm.example.com:/cvs/example git.example.org:/example svn.example.org:/users/*:no +; If use the option allowed_scms above for allowing / denying SCM, default: true +; allowed_scms_use_config = true + +; If use hub policy: build_from_scm for allowing / denying SCM, default: false +; notice that if both options are enabled, both assertions will be applied, and user_common +; will be overridden by the policy's result. +; allowed_scms_use_policy = false + ; The mail host to use for sending email notifications smtphost=example.com From a3f19e0f124fb336e62d2ee0613bac6c362ead27 Mon Sep 17 00:00:00 2001 From: Yu Ming Zhu Date: Wed, 11 Aug 2021 18:02:09 +0000 Subject: [PATCH 2/7] more reasonable parameter name, and more doc strs --- builder/kojid | 16 ++++++++-------- koji/daemon.py | 35 +++++++++++++++++++++++++++++------ vm/kojivmd | 2 +- 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/builder/kojid b/builder/kojid index 3dfc5009..64a733ba 100755 --- a/builder/kojid +++ b/builder/kojid @@ -1643,7 +1643,7 @@ class BuildMavenTask(BaseBuildTask): session=self.session, by_config=self.options.allowed_scms_use_config, by_policy=self.options.allowed_scms_use_policy, - opts=scm_policy_opts) + policy_data=scm_policy_opts) repo_id = opts.get('repo_id') if not repo_id: raise koji.BuildError('A repo_id must be provided') @@ -1721,7 +1721,7 @@ class BuildMavenTask(BaseBuildTask): session=self.session, by_config=self.options.allowed_scms_use_config, by_policy=self.options.allowed_scms_use_policy, - opts=scm_policy_opts) + policy_data=scm_policy_opts) self.run_callbacks('preSCMCheckout', scminfo=patch_scm.get_info(), build_tag=build_tag, scratch=opts.get('scratch'), buildroot=buildroot) @@ -2008,7 +2008,7 @@ class WrapperRPMTask(BaseBuildTask): session=self.session, by_config=self.options.allowed_scms_use_config, by_policy=self.options.allowed_scms_use_policy, - opts={ + policy_data={ 'user_id': self.taskinfo['owner'], 'channel': self.session.getChannel(self.taskinfo['channel_id'], strict=True)['name'], @@ -3006,7 +3006,7 @@ class ImageTask(BaseTaskHandler): session=self.session, by_config=self.options.allowed_scms_use_config, by_policy=self.options.allowed_scms_use_policy, - opts={ + policy_data={ 'user_id': self.taskinfo['owner'], 'channel': self.session.getChannel(self.taskinfo['channel_id'], strict=True)['name'], @@ -3489,7 +3489,7 @@ class LiveMediaTask(ImageTask): session=self.session, by_config=self.options.allowed_scms_use_config, by_policy=self.options.allowed_scms_use_policy, - opts={ + policy_data={ 'user_id': self.taskinfo['owner'], 'channel': self.session.getChannel(self.taskinfo['channel_id'], strict=True)['name'], @@ -3745,7 +3745,7 @@ class OzImageTask(BaseTaskHandler): session=self.session, by_config=self.options.allowed_scms_use_config, by_policy=self.options.allowed_scms_use_policy, - opts={ + policy_data={ 'user_id': self.taskinfo['owner'], 'channel': self.session.getChannel(self.taskinfo['channel_id'], strict=True)['name'], @@ -4581,7 +4581,7 @@ class BuildIndirectionImageTask(OzImageTask): session=self.session, by_config=self.options.allowed_scms_use_config, by_policy=self.options.allowed_scms_use_policy, - opts={ + policy_data={ 'user_id': self.taskinfo['owner'], 'channel': self.session.getChannel(self.taskinfo['channel_id'], strict=True)['name'], @@ -5002,7 +5002,7 @@ class BuildSRPMFromSCMTask(BaseBuildTask): session=self.session, by_config=self.options.allowed_scms_use_config, by_policy=self.options.allowed_scms_use_policy, - opts={ + policy_data={ 'user_id': self.taskinfo['owner'], 'channel': self.session.getChannel(self.taskinfo['channel_id'], strict=True)['name'], diff --git a/koji/daemon.py b/koji/daemon.py index 3eba1a7e..00c2dc96 100644 --- a/koji/daemon.py +++ b/koji/daemon.py @@ -327,14 +327,34 @@ class SCM(object): # return parsed values return (scheme, user, netloc, path, query, fragment) - def assert_allowed(self, allowed='', session=None, by_config=True, by_policy=False, opts=None): + def assert_allowed(self, allowed='', session=None, by_config=True, by_policy=False, + policy_data=None): + """ + Check whether this scm is allowed and apply options by either/both approach below: + + - "allowed_scms" from config file, see + :func:`~koji.daemon.SCM.assert_allowed_by_config` + - "build_from_scm" hub policy, see :func:`~koji.daemon.SCM.assert_allowed_by_policy` + + When both approaches are applied, the config one will be applied, then the policy one. + + :param str allowed: The allowed_scms config content which is used for by-config approach + :param koji.ClientSession session: The allowed_scms config content which is used for + by-policy approach + :param bool by_config: Using config or not, Default: True + :param bool by_policy: Using policy or not, Default: False + :param dict policy_data: The policy data which will be merged with the generated scm info, + then will be passed to hub call for policy assertion when using + policy. + :raises koji.BuildError: if the scm is denied. + """ if by_config: self.assert_allowed_by_config(allowed or '') if by_policy: if session is None: raise koji.ConfigurationError( 'When allowed SCM assertion is by policy, session must be passed in.') - self.assert_allowed_by_policy(session, **(opts or {})) + self.assert_allowed_by_policy(session, **(policy_data or {})) def assert_allowed_by_config(self, allowed): """ @@ -405,7 +425,7 @@ class SCM(object): raise koji.BuildError( '%s:%s is not in the list of allowed SCMs' % (self.host, self.repository)) - def assert_allowed_by_policy(self, session, **opts): + def assert_allowed_by_policy(self, session, **extra_data): """ Check this scm against hub policy: build_from_scm and apply options @@ -421,13 +441,16 @@ class SCM(object): - scmrevision - scmtype - More keys could be added in opts. You can pass any reasonable data which could be handled - by policy tests, like: + More keys could be added as kwargs(extra_data). You can pass any reasonable data which + could be handled by policy tests, like: - scratch (if the task is scratch) - channel (which channel the task is assigned) - user_id (the task owner) + If the key in extra_data is one of scm_* listed above, it will override the one generated + from scminfo. + The format of the action returned from build_from_scm could be one of following forms:: allow [use_common] [source_cmd] @@ -457,7 +480,7 @@ class SCM(object): if not k.startswith('scm'): k = 'scm' + k policy_data[k] = v - policy_data.update(opts) + policy_data.update(extra_data) result = (session.host.evalPolicy('build_from_scm', policy_data) or '').split() is_allowed = result and result[0].lower() in ('yes', 'true', 'allow', 'allowed') if not is_allowed: diff --git a/vm/kojivmd b/vm/kojivmd index 75ab017d..5a6612dc 100755 --- a/vm/kojivmd +++ b/vm/kojivmd @@ -332,7 +332,7 @@ class WinBuildTask(MultiPlatformTask): session=self.session, by_config=self.options.allowed_scms_use_config, by_policy=self.options.allowed_scms_use_policy, - opts={ + policy_data={ 'user_id': self.taskinfo['owner'], 'channel': self.session.getChannel(self.taskinfo['channel_id'], strict=True)['name'], From f1ba3b0ec0af1b35354a98ed2332778557932ffe Mon Sep 17 00:00:00 2001 From: Yu Ming Zhu Date: Wed, 11 Aug 2021 18:06:06 +0000 Subject: [PATCH 3/7] update doc --- docs/source/writing_koji_code.rst | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/source/writing_koji_code.rst b/docs/source/writing_koji_code.rst index 04b9a9c6..267be2ee 100644 --- a/docs/source/writing_koji_code.rst +++ b/docs/source/writing_koji_code.rst @@ -460,12 +460,21 @@ If you your build needs to check out code from a Source Control Manager ``koji/daemon.py``. They take a specially formed URL as an argument to the constructor. Here's an example use. The second line is important, it makes sure the SCM is in the whitelist of SCMs allowed in -``/etc/kojid/kojid.conf``. +``/etc/kojid/kojid.conf`` or in ``build_from_scm`` section of hub policy. :: scm = SCM(url) - scm.assert_allowed(self.options.allowed_scms) + scm.assert_allowed(allowed=self.options.allowed_scms, + session=self.session, + by_config=self.options.allowed_scms_use_config, + by_policy=self.options.allowed_scms_use_policy, + policy_data={ + 'user_id': self.taskinfo['owner'], + 'channel': self.session.getChannel(self.taskinfo['channel_id'], + strict=True)['name'], + 'scratch': opts.get('scratch') + })) directory = scm.checkout('/checkout/path', session, uploaddir, logfile) Checking out takes 4 arguments: where to checkout, a session object From 601cd33902ee2d8c88cc3654c8808320cee9d0b6 Mon Sep 17 00:00:00 2001 From: Yu Ming Zhu Date: Wed, 11 Aug 2021 17:23:00 +0000 Subject: [PATCH 4/7] use scm_ as the prefix instead of scm for scminfo --- hub/kojixmlrpc.py | 6 +++--- koji/daemon.py | 27 +++++++++++++-------------- tests/test_scm.py | 38 +++++++++++++++++++------------------- 3 files changed, 35 insertions(+), 36 deletions(-) diff --git a/hub/kojixmlrpc.py b/hub/kojixmlrpc.py index 196e4730..1263c7d1 100644 --- a/hub/kojixmlrpc.py +++ b/hub/kojixmlrpc.py @@ -526,9 +526,9 @@ _default_policies = { ''', 'build_from_scm': ''' has_perm admin :: allow - # match scmtype CVS CVS+SSH && match scmhost scm.example.com && match scmrepository /cvs/example :: allow - # match scmtype GIT GIT+SSH && match scmhost git.example.org && match scmrepository /example :: allow - # match scmtype SVN SVN+SSH && match scmhost svn.example.org && match scmrepository /users/* :: allow + # match scm_type CVS CVS+SSH && match scm_host scm.example.com && match scm_repository /cvs/example :: allow + # match scm_type GIT GIT+SSH && match scm_host git.example.org && match scm_repository /example :: allow + # match scm_type SVN SVN+SSH && match scm_host svn.example.org && match scm_repository /users/* :: allow all :: deny ''', # noqa: E501 'package_list': ''' diff --git a/koji/daemon.py b/koji/daemon.py index 00c2dc96..774172e9 100644 --- a/koji/daemon.py +++ b/koji/daemon.py @@ -26,6 +26,7 @@ import errno import hashlib import logging import os +import re import signal import subprocess import sys @@ -429,17 +430,17 @@ class SCM(object): """ Check this scm against hub policy: build_from_scm and apply options - The policy data is the combination of scminfo with scm prefix and kwargs. + The policy data is the combination of scminfo with scm_ prefix and kwargs. It should at least contain following keys: - - scmurl - - scmscheme - - scmuser - - scmhost - - scmrepository - - scmmodule - - scmrevision - - scmtype + - scm_url + - scm_scheme + - scm_user + - scm_host + - scm_repository + - scm_module + - scm_revision + - scm_type More keys could be added as kwargs(extra_data). You can pass any reasonable data which could be handled by policy tests, like: @@ -465,8 +466,8 @@ class SCM(object): build_from_scm = bool scratch :: allow none - match scmhost scm.example.com :: allow use_common make sources - match scmhost scm2.example.com :: allow + match scm_host scm.example.com :: allow use_common make sources + match scm_host scm2.example.com :: allow all :: deny @@ -477,9 +478,7 @@ class SCM(object): """ policy_data = {} for k, v in six.iteritems(self.get_info()): - if not k.startswith('scm'): - k = 'scm' + k - policy_data[k] = v + policy_data[re.sub(r'^(scm_?)?', 'scm_', k)] = v policy_data.update(extra_data) result = (session.host.evalPolicy('build_from_scm', policy_data) or '').split() is_allowed = result and result[0].lower() in ('yes', 'true', 'allow', 'allowed') diff --git a/tests/test_scm.py b/tests/test_scm.py index 2ce67b46..1a96143a 100644 --- a/tests/test_scm.py +++ b/tests/test_scm.py @@ -14,22 +14,22 @@ from koji.daemon import SCM policy = { 'one': ''' - match scmhost goodserver :: allow none - match scmhost badserver :: deny - match scmhost maybeserver && match scmrepository /badpath/* :: deny + match scm_host goodserver :: allow none + match scm_host badserver :: deny + match scm_host maybeserver && match scm_repository /badpath/* :: deny all :: allow ''', 'two': ''' - match scmhost default :: allow - match scmhost nocommon :: allow - match scmhost common :: allow use_common - match scmhost srccmd :: allow fedpkg sources - match scmhost nosrc :: allow none - match scmhost mixed && match scmrepository /foo/* :: allow - match scmhost mixed && match scmrepository /bar/* :: allow use_common - match scmhost mixed && match scmrepository /baz/* :: allow fedpkg sources - match scmhost mixed && match scmrepository /foobar/* :: allow use_common fedpkg sources - match scmhost mixed && match scmrepository /foobaz/* :: allow use_common none + match scm_host default :: allow + match scm_host nocommon :: allow + match scm_host common :: allow use_common + match scm_host srccmd :: allow fedpkg sources + match scm_host nosrc :: allow none + match scm_host mixed && match scm_repository /foo/* :: allow + match scm_host mixed && match scm_repository /bar/* :: allow use_common + match scm_host mixed && match scm_repository /baz/* :: allow fedpkg sources + match scm_host mixed && match scm_repository /foobar/* :: allow use_common fedpkg sources + match scm_host mixed && match scm_repository /foobaz/* :: allow use_common none ''' } @@ -376,42 +376,42 @@ class TestSCM(unittest.TestCase): url = "git://default/koji.git#1234" scm = SCM(url) - # match scmhost default :: allow + # match scm_host default :: allow scm.assert_allowed(allowed=config, session=session, by_config=True, by_policy=True) self.assertEqual(scm.use_common, False) self.assertIsNone(scm.source_cmd) url = "git://mixed/foo/koji.git#1234" scm = SCM(url) - # match scmhost mixed && match scmrepository /foo/* :: allow + # match scm_host mixed && match scm_repository /foo/* :: allow scm.assert_allowed(allowed=config, session=session, by_config=True, by_policy=True) self.assertEqual(scm.use_common, False) self.assertEqual(scm.source_cmd, ['make', 'sources']) url = "git://mixed/bar/koji.git#1234" scm = SCM(url) - # match scmhost mixed && match scmrepository /bar/* :: allow use_common + # match scm_host mixed && match scm_repository /bar/* :: allow use_common scm.assert_allowed(allowed=config, session=session, by_config=True, by_policy=True) self.assertEqual(scm.use_common, True) self.assertEqual(scm.source_cmd, ['make', 'sources']) url = "git://mixed/baz/koji.git#1234" scm = SCM(url) - # match scmhost mixed && match scmrepository /baz/* :: allow fedpkg sources + # match scm_host mixed && match scm_repository /baz/* :: allow fedpkg sources scm.assert_allowed(allowed=config, session=session, by_config=True, by_policy=True) self.assertEqual(scm.use_common, False) self.assertEqual(scm.source_cmd, ['fedpkg', 'sources']) url = "git://mixed/foobar/koji.git#1234" scm = SCM(url) - # match scmhost mixed && match scmrepository /foobar/* :: allow use_common fedpkg sources + # match scm_host mixed && match scm_repository /foobar/* :: allow use_common fedpkg sources scm.assert_allowed(allowed=config, session=session, by_config=True, by_policy=True) self.assertEqual(scm.use_common, True) self.assertEqual(scm.source_cmd, ['fedpkg', 'sources']) url = "git://mixed/foobaz/koji.git#1234" scm = SCM(url) - # match scmhost mixed && match scmrepository /foobaz/* :: allow use_common none + # match scm_host mixed && match scm_repository /foobaz/* :: allow use_common none scm.assert_allowed(allowed=config, session=session, by_config=True, by_policy=True) self.assertEqual(scm.use_common, True) self.assertIsNone(scm.source_cmd) From cdbebd963d8f05076376d29473360b1ca4b06023 Mon Sep 17 00:00:00 2001 From: Yu Ming Zhu Date: Wed, 11 Aug 2021 18:53:57 +0000 Subject: [PATCH 5/7] [hub] add non-host evalPolicy API --- hub/kojihub.py | 21 +++++++++++++++++---- koji/daemon.py | 2 +- tests/test_scm.py | 6 +++--- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/hub/kojihub.py b/hub/kojihub.py index 7c7a5358..3f50c1f3 100644 --- a/hub/kojihub.py +++ b/hub/kojihub.py @@ -10099,6 +10099,20 @@ def check_policy(name, data, default='deny', strict=False, force=False): raise koji.ActionNotAllowed(err_str) +def eval_policy(name, data): + """Evaluate named policy with given data and return the result + + :param str name: the policy name + :param dict data: the policy data + :returns the action as a string + :raises koji.GenericError if the policy is empty or not found + """ + ruleset = context.policy.get(name) + if not ruleset: + raise koji.GenericError("no such policy: %s" % name) + return ruleset.apply(data) + + def policy_data_from_task(task_id): """Calculate policy data from task id @@ -10653,6 +10667,8 @@ class RootExports(object): q += """ ORDER BY id DESC LIMIT 1""" return _singleRow(q, values, fields, strict=True) + evalPolicy = staticmethod(eval_policy) + def makeTask(self, *args, **opts): """Creates task manually. This is mainly for debugging, only an admin can make arbitrary tasks. You need to supply all *args and **opts @@ -14622,10 +14638,7 @@ class HostExports(object): """Evaluate named policy with given data and return the result""" host = Host() host.verify() - ruleset = context.policy.get(name) - if not ruleset: - raise koji.GenericError("no such policy: %s" % name) - return ruleset.apply(data) + return eval_policy(name, data) def newBuildRoot(self, repo, arch, task_id=None): host = Host() diff --git a/koji/daemon.py b/koji/daemon.py index 774172e9..786e03bc 100644 --- a/koji/daemon.py +++ b/koji/daemon.py @@ -480,7 +480,7 @@ class SCM(object): for k, v in six.iteritems(self.get_info()): policy_data[re.sub(r'^(scm_?)?', 'scm_', k)] = v policy_data.update(extra_data) - result = (session.host.evalPolicy('build_from_scm', policy_data) or '').split() + result = (session.evalPolicy('build_from_scm', policy_data) or '').split() is_allowed = result and result[0].lower() in ('yes', 'true', 'allow', 'allowed') if not is_allowed: raise koji.BuildError( diff --git a/tests/test_scm.py b/tests/test_scm.py index 1a96143a..abcda692 100644 --- a/tests/test_scm.py +++ b/tests/test_scm.py @@ -259,7 +259,7 @@ class TestSCM(unittest.TestCase): "git://maybeserver/..//badpath/project#1234", ] session = mock.MagicMock() - session.host.evalPolicy.side_effect = FakePolicy(policy['one']).evalPolicy + session.evalPolicy.side_effect = FakePolicy(policy['one']).evalPolicy for url in good: scm = SCM(url) scm.assert_allowed(session=session, by_config=False, by_policy=True) @@ -271,7 +271,7 @@ class TestSCM(unittest.TestCase): def test_opts_by_policy(self): session = mock.MagicMock() - session.host.evalPolicy.side_effect = FakePolicy(policy['two']).evalPolicy + session.evalPolicy.side_effect = FakePolicy(policy['two']).evalPolicy url = "git://default/koji.git#1234" scm = SCM(url) @@ -372,7 +372,7 @@ class TestSCM(unittest.TestCase): ''' session = mock.MagicMock() - session.host.evalPolicy.side_effect = FakePolicy(policy['two']).evalPolicy + session.evalPolicy.side_effect = FakePolicy(policy['two']).evalPolicy url = "git://default/koji.git#1234" scm = SCM(url) From b35811ed566cd8a339849a5512299654028f698e Mon Sep 17 00:00:00 2001 From: Yu Ming Zhu Date: Mon, 16 Aug 2021 14:20:33 +0000 Subject: [PATCH 6/7] fix config typo --- builder/kojid | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/builder/kojid b/builder/kojid index 64a733ba..39632ad8 100755 --- a/builder/kojid +++ b/builder/kojid @@ -6429,8 +6429,8 @@ def get_options(): 'mock_bootstrap_image': False, 'pkgurl': None, 'allowed_scms': '', - 'allowed_scms_by_config': True, - 'allowed_scms_by_policy': False, + 'allowed_scms_use_config': True, + 'allowed_scms_use_policy': False, 'scm_credentials_dir': None, 'support_rpm_source_layout': True, 'yum_proxy': None, @@ -6459,7 +6459,7 @@ def get_options(): elif name in ['offline_retry', 'use_createrepo_c', 'createrepo_skip_stat', 'createrepo_update', 'use_fast_upload', 'support_rpm_source_layout', 'build_arch_can_fail', 'no_ssl_verify', 'log_timestamps', - 'allow_noverifyssl', 'allowed_scms_by_config', 'allowed_scms_by_policy']: + 'allow_noverifyssl', 'allowed_scms_use_config', 'allowed_scms_use_policy']: defaults[name] = config.getboolean('kojid', name) elif name in ['plugin', 'plugins']: defaults['plugin'] = value.split() From bc272c0532552932f6004e9e1a40dfbbf7440107 Mon Sep 17 00:00:00 2001 From: Yu Ming Zhu Date: Wed, 18 Aug 2021 08:43:50 +0000 Subject: [PATCH 7/7] [doc][defining_hub_policies] update the doc --- docs/source/access_controls.rst | 10 ++++++++++ docs/source/defining_hub_policies.rst | 25 +++++++++++++++++++++++++ docs/source/server_howto.rst | 5 +++++ koji/daemon.py | 4 ++-- 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/docs/source/access_controls.rst b/docs/source/access_controls.rst index 201c2641..d770f8d8 100644 --- a/docs/source/access_controls.rst +++ b/docs/source/access_controls.rst @@ -15,6 +15,8 @@ username/password but it has its limitations which you should be aware of. Details can be found at :ref:`auth-config` +.. _allowed-scms: + Allowed SCMs ============ @@ -24,6 +26,13 @@ We recommend that every production environment choose a limited set of trusted s Details of the ``allowed_scms`` option are covered under :ref:`scm-config` +We also provides ``build_from_scm`` hub policy for the same purpose, you can choose either/both +of the two approaches by the switch options in ``/etc/kojid.conf`` per build: + + * ``allowed_scms_use_config``, default: ``true`` + * ``allowed_scms_use_policy``, default: ``false`` + +For more details of the ``build_from_scm``, please read :doc:`defining_hub_policies`. Hub Policies ============ @@ -43,6 +52,7 @@ Examples of access control polices are: * vm: control which windows build tasks are allowed * dist_repo: control which distRepo tasks are allowed * build_from_srpm: control whether builds from srpm are allowed +* build_from_scm: control whether builds from the SCM are allowed and the behavior of the SCM * build_from_repo_id: control whether builds from user-specified repos ids are allowed Note that not all policies are access control policies. diff --git a/docs/source/defining_hub_policies.rst b/docs/source/defining_hub_policies.rst index d0ccc0ff..21bf8aae 100644 --- a/docs/source/defining_hub_policies.rst +++ b/docs/source/defining_hub_policies.rst @@ -7,6 +7,8 @@ in the system. At present, policy allows you to control: * tag/untag/move operations * allowing builds from srpm +* allowing builds from SCM, and managing properties/behaviors related to the SCM + if it is allowed * allowing builds from expired repos * managing the package list for a tag * managing which channel a task goes to @@ -19,6 +21,11 @@ Policy configuration is optional. If you don't define one, then by default: * tag/untag/move operations are governed by tag locks/permissions * builds from srpm are only allowed for admins +* builds from any SCM are only allowed for admins. It's used when + ``allowed_scms_use_policy`` is ``true`` in ``/etc/kojid.conf`` of the builders + (``false`` by default). And the SCM's properies: ``use_common`` and + ``source_cmd`` are set to their default values: ``False`` and + ``['make', 'source']`` * builds from expired repos are only allowed for admins * only admins and users with ``tag`` permission may modify package lists * tasks go to the default channel @@ -126,6 +133,7 @@ The system currently looks for the following policies * ``tag``: checked during tag/untag/move operations * ``build_from_srpm``: checked when a build from srpm (not an SCM reference) is requested. +* ``build_from_scm``: checked when a build task from SCM is executing on builder * ``build_from_repo_id``: checked when a build from a specified repo id is requested * ``package_list``: checked when the package list for a tag is modified @@ -193,6 +201,23 @@ different: ``adjust -`` * decrement default priority +The **build_from_scm** policy is used to assert if the SCM is allowed or not, +like the basic allow/deny one. It is also used to manage the SCM's properties as +the same as the ``allowed_scms`` option of the koji builder. The actions could +be defined as: + +``allow [use_common] []`` + * allow the SCM + * use(clone) the /common repo when ``use_common`` follows ``allow`` + * ```` is a *optional* shell command for preparing the source + between checkout and srpm build. If it is omitted, it will follow the + default value: ``make source``. The explicit value: ``none`` means **No** + ``source_cmd`` is defined. + +``deny []`` + * disallow the SCM + * ```` is the error message which is shown as the task result + Available tests =============== ``true`` diff --git a/docs/source/server_howto.rst b/docs/source/server_howto.rst index 29f811b0..b386688f 100644 --- a/docs/source/server_howto.rst +++ b/docs/source/server_howto.rst @@ -1216,6 +1216,11 @@ SCM checkout can contain multiple spec files (checkouted or created by ``source_cmd``). In such case spec file named same as a checkout directory will be selected. +.. note:: + We provide ``build_from_scm`` hub policy as an equivalent in version 1.26.0. + + For more details, please refer to :ref:`allowed-scms` and + :doc:`Defining Hub Policies `. Add the host to the createrepo channel -------------------------------------- diff --git a/koji/daemon.py b/koji/daemon.py index 786e03bc..c94e1c5c 100644 --- a/koji/daemon.py +++ b/koji/daemon.py @@ -454,8 +454,8 @@ class SCM(object): The format of the action returned from build_from_scm could be one of following forms:: - allow [use_common] [source_cmd] - deny [reason] + allow [use_common] [] + deny [] If use_common is not set, use_common property is False. If source_cmd is none, it will be parsed as None. If it not set, the default value: