Add disallowed-labels tenant option

To allow a tenant to use any labels *except* some pattern, add the
disallowed-labels tenant option.  Both this and the allowed-labels
option use re2, and therefore lookahead assertions are not supported.
A complimentary option to allowed-labels is the only way to support
this use case.

Change-Id: Ic722b1d2b0b609ec7de583dab159094159f00630
This commit is contained in:
James E. Blair 2020-02-04 13:46:49 -08:00
parent fbc0bbd314
commit 61e5c3a0f9
8 changed files with 103 additions and 22 deletions

View File

@ -293,9 +293,21 @@ configuration. Some examples of tenant definitions are:
.. attr:: allowed-labels
:default: []
The list of labels regexp a tenant can use in job's nodeset. When set,
this setting can be used to restrict what labels a tenant can use.
Without this setting, the tenant can use any labels.
The list of labels (as strings or regular expressions) a tenant
can use in a job's nodeset. When set, this setting can be used
to restrict what labels a tenant can use. Without this setting,
the tenant can use any labels.
.. attr:: disallowed-labels
:default: []
The list of labels (as strings or regular expressions) a tenant
is forbidden to use in a job's nodeset. When set, this setting
can be used to restrict what labels a tenant can use. Without
this setting, the tenant can use any labels permitted by
:attr:`tenant.allowed-labels`. This check is applied after the
check for `allowed-labels` and may therefore be used to further
restrict the set of permitted labels.
.. attr:: report-build-page
:default: false

View File

@ -0,0 +1,6 @@
---
features:
- |
A new tenant option :attr:`tenant.disallowed-labels`
(similar to :attr:`tenant.allowed-labels`)
can be used to restrict what labels a tenant has access to.

View File

@ -19,6 +19,8 @@
name: tenant-two
max-nodes-per-job: 10
allowed-triggers: gerrit
disallowed-labels:
- tenant-one-.*
source:
gerrit:
config-projects:

View File

@ -3907,6 +3907,26 @@ class TestAllowedLabels(AnsibleZuulTestCase):
A.messages[0],
"A should fail because of allowed-labels")
def test_disallowed_labels(self):
in_repo_conf = textwrap.dedent(
"""
- job:
name: test
nodeset:
nodes:
- name: controller
label: tenant-one-label
""")
file_dict = {'zuul.d/test.yaml': in_repo_conf}
A = self.fake_gerrit.addFakeChange(
'tenant-two-config', 'master', 'A', files=file_dict)
self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
self.waitUntilSettled()
self.assertIn(
'Label named "tenant-one-label" is not part of the allowed',
A.messages[0],
"A should fail because of disallowed-labels")
class TestPragma(ZuulTestCase):
tenant_config_file = 'config/pragma/main.yaml'

View File

@ -955,13 +955,14 @@ class TestWebMultiTenant(BaseTestWeb):
def test_web_labels_allowed_list(self):
labels = ["tenant-one-label", "fake", "tenant-two-label"]
self.fake_nodepool.registerLauncher(labels, "FakeLauncher2")
# Tenant-one has label restriction in place
# Tenant-one has label restriction in place on tenant-two
res = self.get_url('api/tenant/tenant-one/labels').json()
self.assertEqual([{'name': 'fake'}, {'name': 'tenant-one-label'}], res)
# Tenant-two does not
# Tenant-two has label restriction in place on tenant-one
expected = ["label1", "fake", "tenant-two-label"]
res = self.get_url('api/tenant/tenant-two/labels').json()
self.assertEqual(
list(map(lambda x: {'name': x}, sorted(labels + ["label1"]))), res)
list(map(lambda x: {'name': x}, sorted(expected))), res)
class TestWebSecrets(BaseTestWeb):

View File

@ -81,13 +81,21 @@ class UnknownConnection(Exception):
class LabelForbiddenError(Exception):
def __init__(self, label, allowed_labels):
def __init__(self, label, allowed_labels, disallowed_labels):
message = textwrap.dedent("""\
Label named "{label}" is not part of the allowed
labels ({allowed_labels}) for this tenant.""")
# Make a string that looks like "a, b and not c, d" if we have
# both allowed and disallowed labels.
labels = ", ".join(allowed_labels or [])
if allowed_labels and disallowed_labels:
labels += ' and '
if disallowed_labels:
labels += 'not '
labels += ", ".join(disallowed_labels)
message = textwrap.fill(message.format(
label=label,
allowed_labels=", ".join(allowed_labels)))
allowed_labels=labels))
super(LabelForbiddenError, self).__init__(message)
@ -500,13 +508,25 @@ class NodeSetParser(object):
node_names = set()
group_names = set()
allowed_labels = self.pcontext.tenant.allowed_labels
disallowed_labels = self.pcontext.tenant.disallowed_labels
for conf_node in as_list(conf['nodes']):
allowed = True
if allowed_labels:
if not [True for allowed_label in allowed_labels if
re2.match(allowed_label, conf_node['label'])]:
raise LabelForbiddenError(
label=conf_node['label'],
allowed_labels=allowed_labels)
allowed = False
for pattern in allowed_labels:
if re2.match(pattern, conf_node['label']):
allowed = True
break
if disallowed_labels:
for pattern in disallowed_labels:
if re2.match(pattern, conf_node['label']):
allowed = False
break
if not allowed:
raise LabelForbiddenError(
label=conf_node['label'],
allowed_labels=allowed_labels,
disallowed_labels=disallowed_labels)
for name in as_list(conf_node['name']):
if name in node_names:
raise DuplicateNodeError(name, conf_node['name'])
@ -1456,6 +1476,7 @@ class TenantParser(object):
'allowed-triggers': to_list(str),
'allowed-reporters': to_list(str),
'allowed-labels': to_list(str),
'disallowed-labels': to_list(str),
'default-parent': str,
'default-ansible-version': vs.Any(str, float),
'admin-rules': to_list(str),
@ -1484,6 +1505,7 @@ class TenantParser(object):
tenant.allowed_triggers = conf.get('allowed-triggers')
tenant.allowed_reporters = conf.get('allowed-reporters')
tenant.allowed_labels = conf.get('allowed-labels')
tenant.disallowed_labels = conf.get('disallowed-labels')
tenant.default_base_job = conf.get('default-parent', 'base')
tenant.unparsed_config = conf

View File

@ -499,10 +499,10 @@ class RPCListener(object):
if not tenant:
job.sendWorkComplete(json.dumps(None))
return
labels = tenant.allowed_labels
if not labels:
labels = []
job.sendWorkComplete(json.dumps(labels))
ret = {}
ret['allowed_labels'] = tenant.allowed_labels or []
ret['disallowed_labels'] = tenant.disallowed_labels or []
job.sendWorkComplete(json.dumps(ret))
def handle_pipeline_list(self, job):
args = json.loads(job.arguments)

View File

@ -736,15 +736,33 @@ class ZuulWebAPI(object):
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
def labels(self, tenant):
job = self.rpc.submitJob('zuul:allowed_labels_get', {'tenant': tenant})
allowed_labels = json.loads(job.data[0])
if allowed_labels is None:
data = json.loads(job.data[0])
if data is None:
raise cherrypy.HTTPError(404, 'Tenant %s does not exist.' % tenant)
# TODO(jeblair): The first case can be removed after 3.16.0 is
# released.
if isinstance(data, list):
allowed_labels = data
disallowed_labels = []
else:
allowed_labels = data['allowed_labels']
disallowed_labels = data['disallowed_labels']
labels = set()
for launcher in self.zk.getRegisteredLaunchers():
for label in launcher.supported_labels:
if not allowed_labels or (
[True for allowed_label in allowed_labels if
re2.match(allowed_label, label)]):
allowed = True
if allowed_labels:
allowed = False
for pattern in allowed_labels:
if re2.match(pattern, label):
allowed = True
break
if disallowed_labels:
for pattern in disallowed_labels:
if re2.match(pattern, label):
allowed = False
break
if allowed:
labels.add(label)
ret = [{'name': label} for label in sorted(labels)]
resp = cherrypy.response