Add 'allow-secrets' pipeline attribute

This permits an operator to specify that a given pipeline should
never run a job with secrets.  This may be used, for example, to
ensure that no one adds, say, the pypi-upload job to a check
pipeline and then uses that to expose credentials.

Change-Id: I606a76fe9ed19bb87d78f07195fb3950805e8726
diff --git a/tests/fixtures/config/ansible/git/common-config/zuul.yaml b/tests/fixtures/config/ansible/git/common-config/zuul.yaml
index 3678f94..6d829e4 100644
--- a/tests/fixtures/config/ansible/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/ansible/git/common-config/zuul.yaml
@@ -2,6 +2,7 @@
     name: check
     manager: independent
     source: gerrit
+    allow-secrets: true
     trigger:
       gerrit:
         - event: patchset-created
diff --git a/tests/unit/test_model.py b/tests/unit/test_model.py
index fc2757c..f906095 100644
--- a/tests/unit/test_model.py
+++ b/tests/unit/test_model.py
@@ -616,6 +616,43 @@
                 "Project project2 is not allowed to run job job"):
             item.freezeJobGraph()
 
+    def test_job_pipeline_allow_secrets(self):
+        self.pipeline.allow_secrets = False
+        job = configloader.JobParser.fromYaml(self.tenant, self.layout, {
+            '_source_context': self.context,
+            '_start_mark': self.start_mark,
+            'name': 'job',
+        })
+        auth = model.AuthContext()
+        auth.secrets.append('foo')
+        job.auth = auth
+
+        self.layout.addJob(job)
+
+        project_config = configloader.ProjectParser.fromYaml(
+            self.tenant, self.layout, [{
+                '_source_context': self.context,
+                '_start_mark': self.start_mark,
+                'name': 'project',
+                'gate': {
+                    'jobs': [
+                        'job'
+                    ]
+                }
+            }]
+        )
+        self.layout.addProjectConfig(project_config)
+
+        change = model.Change(self.project)
+        # Test master
+        change.branch = 'master'
+        item = self.queue.enqueueChange(change)
+        item.current_build_set.layout = self.layout
+        with testtools.ExpectedException(
+                Exception,
+                "Pipeline gate does not allow jobs with secrets"):
+            item.freezeJobGraph()
+
 
 class TestJobTimeData(BaseTestCase):
     def setUp(self):
diff --git a/zuul/configloader.py b/zuul/configloader.py
index 21f0f64..64c8db4 100644
--- a/zuul/configloader.py
+++ b/zuul/configloader.py
@@ -608,6 +608,7 @@
                     'footer-message': str,
                     'dequeue-on-new-patchset': bool,
                     'ignore-dependencies': bool,
+                    'allow-secrets': bool,
                     'disable-after-consecutive-failures':
                         vs.All(int, vs.Range(min=1)),
                     'window': window,
@@ -655,6 +656,7 @@
             'dequeue-on-new-patchset', True)
         pipeline.ignore_dependencies = conf.get(
             'ignore-dependencies', False)
+        pipeline.allow_secrets = conf.get('allow-secrets', False)
 
         for conf_key, action in PipelineParser.reporter_actions.items():
             reporter_set = []
diff --git a/zuul/model.py b/zuul/model.py
index 846463b..cd63a94 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -121,6 +121,7 @@
         self.success_message = None
         self.footer_message = None
         self.start_message = None
+        self.allow_secrets = False
         self.dequeue_on_new_patchset = True
         self.ignore_dependencies = False
         self.manager = None
@@ -2321,7 +2322,9 @@
     def addProjectConfig(self, project_config):
         self.project_configs[project_config.name] = project_config
 
-    def _createJobGraph(self, change, job_list, job_graph):
+    def _createJobGraph(self, item, job_list, job_graph):
+        change = item.change
+        pipeline = item.pipeline
         for jobname in job_list.jobs:
             # This is the final job we are constructing
             frozen_job = None
@@ -2360,6 +2363,11 @@
                 change.project.name not in frozen_job.allowed_projects):
                 raise Exception("Project %s is not allowed to run job %s" %
                                 (change.project.name, frozen_job.name))
+            if ((not pipeline.allow_secrets) and frozen_job.auth and
+                frozen_job.auth.secrets):
+                raise Exception("Pipeline %s does not allow jobs with "
+                                "secrets (job %s)" % (
+                                    pipeline.name, frozen_job.name))
             job_graph.addJob(frozen_job)
 
     def createJobGraph(self, item):
@@ -2371,7 +2379,7 @@
         if project_config and item.pipeline.name in project_config.pipelines:
             project_job_list = \
                 project_config.pipelines[item.pipeline.name].job_list
-            self._createJobGraph(item.change, project_job_list, ret)
+            self._createJobGraph(item, project_job_list, ret)
         return ret