Add allow-base-jobs tenant config attribute

Admins may want to allow tenants to utilize some of the powers
of config-projects without allowing them to define base jobs.
Base jobs often perform security-critical actions which should
not be omitted under any circumstance.  To facilitate this,
allow for the prohibition of base jobs within the tenant
definition.

Change-Id: I2382a35a2d683175508fbb658609e0035028b69e
This commit is contained in:
James E. Blair
2026-06-11 13:38:18 -07:00
parent 6d521f90c0
commit cfc670a460
9 changed files with 85 additions and 10 deletions
+12 -2
View File
@@ -139,8 +139,8 @@ configuration. Some examples of tenant definitions are:
.. attr:: <project>
The config-projects have an additional config option that
may be specified optionally.
The config-projects have the following optional additional
config options:
.. attr:: load-branch
:default: master
@@ -149,6 +149,16 @@ configuration. Some examples of tenant definitions are:
default config projects load Zuul configuration only
from the master branch.
.. attr:: allow-base-jobs
:type: bool
:default: true
Set to ``false`` to disallow base job definitions in this
project. Base jobs are typically critical for the
security of the entire Zuul system. This setting permits
the use of config-projects for features (such as pipeline
definition) without the ability to define base jobs.
.. attr:: untrusted-projects
A list of projects to be treated as untrusted in this tenant.
@@ -0,0 +1,8 @@
---
features:
- |
The :attr:`tenant.config-projects.<project>.allow-base-jobs`
tenant configuration attribute has been added. This facilitates
the configuration of a tenant with the ability to use all of the
aspects of a config-project other than defining base jobs when the
definition of a base job may be critical for system-wide security.
@@ -22,3 +22,8 @@
parent: null
tags:
- otherbase
- project:
name: extra-config
check:
jobs: []
+2
View File
@@ -5,5 +5,7 @@
gerrit:
config-projects:
- common-config
- extra-config:
allow-base-jobs: false
untrusted-projects:
- org/project
+23 -1
View File
@@ -8353,7 +8353,29 @@ class TestBaseJobs(ZuulTestCase):
self.assertEqual(A.reported, 1,
"A should report failure")
self.assertEqual(A.patchsets[0]['approvals'][0]['value'], "-1")
self.assertIn('Base jobs must be defined in config projects',
self.assertIn('Base jobs may only be defined',
A.messages[0])
self.assertHistory([])
def test_disallowed_base_job(self):
# Test that we can disable configuring base jobs even in
# config-projects
in_repo_conf = textwrap.dedent(
"""
- job:
name: fail-base
parent: null
""")
file_dict = {'.zuul.yaml': in_repo_conf}
A = self.fake_gerrit.addFakeChange('extra-config', 'master', 'A',
files=file_dict)
self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
self.waitUntilSettled()
self.assertEqual(A.reported, 1,
"A should report failure")
self.assertEqual(A.patchsets[0]['approvals'][0]['value'], "-1")
self.assertIn('Base jobs may only be defined',
A.messages[0])
self.assertHistory([])
+9 -1
View File
@@ -2107,11 +2107,14 @@ class TenantParser(object):
'allow-circular-dependencies': bool,
'implied-branch-matchers': bool,
}
config_project_dict = {str: inner_config_project_dict}
inner_untrusted_project_dict = inner_config_project_dict.copy()
inner_untrusted_project_dict['configure-projects'] = to_list(str)
inner_untrusted_project_dict['allow-reporter-jobs'] = bool
inner_config_project_dict['allow-base-jobs'] = bool
config_project_dict = {str: inner_config_project_dict}
untrusted_project_dict = {str: inner_untrusted_project_dict}
config_project = vs.Any(str, config_project_dict)
@@ -2404,6 +2407,7 @@ class TenantParser(object):
project_implied_branch_matchers = None
project_configure_projects = None
project_allow_reporter_jobs = None
project_allow_base_jobs = None
else:
project_name = list(conf.keys())[0]
project = source.getProject(project_name)
@@ -2480,6 +2484,8 @@ class TenantParser(object):
project_configure_projects = None
project_allow_reporter_jobs = conf[project_name].get(
'allow-reporter-jobs', None)
project_allow_base_jobs = conf[project_name].get(
'allow-base-jobs', None)
tenant_project_config = model.TenantProjectConfig(project)
tenant_project_config.load_classes = frozenset(project_include)
@@ -2501,6 +2507,8 @@ class TenantParser(object):
project_configure_projects
tenant_project_config.allow_reporter_jobs = \
project_allow_reporter_jobs
tenant_project_config.allow_base_jobs = \
project_allow_base_jobs
return tenant_project_config
def _getProjects(self, source, conf, current_include):
+11
View File
@@ -386,6 +386,17 @@ class ProjectNotPermittedError(ConfigurationSyntaxError):
super(ProjectNotPermittedError, self).__init__(message)
class BaseJobNotPermittedError(ConfigurationSyntaxError):
zuul_error_name = 'Base Job Not Permitted'
def __init__(self):
message = textwrap.dedent("""\
Base jobs may only be defined in config projects, and
only if permitted by the tenant configuration.""")
message = textwrap.fill(message)
super(BaseJobNotPermittedError, self).__init__(message)
class AuthZRuleNotFoundError(ConfigurationSyntaxError):
zuul_error_name = 'AuthZ Rule Not Found'
+15 -6
View File
@@ -42,12 +42,13 @@ from zuul import change_matcher
from zuul.exceptions import (
SEVERITY_ERROR,
SEVERITY_WARNING,
OIDCIssuerNotAllowedError,
BaseJobNotPermittedError,
LabelForbiddenError,
LabelNotFoundError,
MaxOIDCTTLError,
MaxTimeoutError,
LabelNotFoundError,
NodesetNotFoundError,
OIDCIssuerNotAllowedError,
PreTimeoutExceedsTimeoutError,
ProjectNotFoundError,
ProjectNotPermittedError,
@@ -9677,6 +9678,7 @@ class TenantProjectConfig(object):
# to configure
self.configure_projects = None
self.allow_reporter_jobs = None
self.allow_base_jobs = None
def canConfigureProject(self, other_tpc, validation_only=False):
if self.trusted:
@@ -9694,6 +9696,13 @@ class TenantProjectConfig(object):
return True
return False
def canDefineBaseJobs(self):
if not self.trusted:
return False
if self.allow_base_jobs is False:
return False
return True
def isAlwaysDynamicBranch(self, branch):
if self.always_dynamic_branches is None:
return False
@@ -10552,10 +10561,10 @@ class Layout(object):
job.post_timeout > self.tenant.max_job_timeout):
raise MaxTimeoutError(job, self.tenant)
if job.isBase() and not self.tenant.isTrusted(
job.source_context.project_canonical_name):
raise Exception(
"Base jobs must be defined in config projects")
source_tpc = self.tenant.getTPC(
job.source_context.project_canonical_name)
if job.isBase() and not source_tpc.canDefineBaseJobs():
raise BaseJobNotPermittedError()
def addJob(self, job):
self._checkAddJob(job)