Test that secrets don't leak into logs

This executes a job which writes a secret into a file in the jobdir,
which is typical of how we would expect many jobs which use secrets
to operate.

It also executes a similar job where ansible fails to write the file,
to test that error-handling code doesn't helpfully leak the secret.

It runs both of those tests with and without '-vvv' set.

It then searches for that secret in all files in the jobdir and
ensures it doesn't show up in any unexpected files.  This includes
the ansible log(s).

Change-Id: Ie6ebe301f256d20e482b5f6c64f3ce2fb2b5135d
diff --git a/tests/fixtures/config/secret-leaks/git/common-config/playbooks/secret-file-fail.yaml b/tests/fixtures/config/secret-leaks/git/common-config/playbooks/secret-file-fail.yaml
new file mode 100644
index 0000000..4984411
--- /dev/null
+++ b/tests/fixtures/config/secret-leaks/git/common-config/playbooks/secret-file-fail.yaml
@@ -0,0 +1,6 @@
+- hosts: all
+  tasks:
+    - copy:
+        content: "{{test_secret.username}} {{test_secret.password}}"
+        dest: "{{zuul.executor.work_root}}/failure-file.txt"
+        group: "hopefullythisgroupdoesnotexist"
\ No newline at end of file
diff --git a/tests/fixtures/config/secret-leaks/git/common-config/playbooks/secret-file.yaml b/tests/fixtures/config/secret-leaks/git/common-config/playbooks/secret-file.yaml
new file mode 100644
index 0000000..24bb61f
--- /dev/null
+++ b/tests/fixtures/config/secret-leaks/git/common-config/playbooks/secret-file.yaml
@@ -0,0 +1,5 @@
+- hosts: all
+  tasks:
+    - copy:
+        content: "{{test_secret.username}} {{test_secret.password}}"
+        dest: "{{zuul.executor.work_root}}/secret-file.txt"
diff --git a/tests/fixtures/config/secret-leaks/git/common-config/zuul.yaml b/tests/fixtures/config/secret-leaks/git/common-config/zuul.yaml
new file mode 100644
index 0000000..4ab198f
--- /dev/null
+++ b/tests/fixtures/config/secret-leaks/git/common-config/zuul.yaml
@@ -0,0 +1,70 @@
+- pipeline:
+    name: check
+    manager: independent
+    post-review: true
+    trigger:
+      gerrit:
+        - event: patchset-created
+    success:
+      gerrit:
+        Verified: 1
+    failure:
+      gerrit:
+        Verified: -1
+
+- pipeline:
+    name: gate
+    manager: dependent
+    success-message: Build succeeded (gate).
+    trigger:
+      gerrit:
+        - event: comment-added
+          approval:
+            - Approved: 1
+    success:
+      gerrit:
+        Verified: 2
+        submit: true
+    failure:
+      gerrit:
+        Verified: -2
+    start:
+      gerrit:
+        Verified: 0
+    precedence: high
+
+- secret:
+    name: test_secret
+    data:
+      username: test-username
+      password: !encrypted/pkcs1-oaep |
+        BFhtdnm8uXx7kn79RFL/zJywmzLkT1GY78P3bOtp4WghUFWobkifSu7ZpaV4NeO0s71YUsi1wGZZ
+        L0LveZjUN0t6OU1VZKSG8R5Ly7urjaSo1pPVIq5Rtt/H7W14Lecd+cUeKb4joeusC9drN3AA8a4o
+        ykcVpt1wVqUnTbMGC9ARMCQP6eopcs1l7tzMseprW4RDNhIuz3CRgd0QBMPl6VDoFgBPB8vxtJw+
+        3m0rqBYZCLZgCXekqlny8s2s92nJMuUABbJOEcDRarzibDsSXsfJt1y+5n7yOURsC7lovMg4GF/v
+        Cl/0YMKjBO5bpv9EM5fToeKYyPGSKQoHOnCYceb3cAVcv5UawcCic8XjhEhp4K7WPdYf2HVAC/qt
+        xhbpjTxG4U5Q/SoppOJ60WqEkQvbXs6n5Dvy7xmph6GWmU/bAv3eUK3pdD3xa2Ue1lHWz3U+rsYr
+        aI+AKYsMYx3RBlfAmCeC1ve2BXPrqnOo7G8tnUvfdYPbK4Aakk0ds/AVqFHEZN+S6hRBmBjLaRFW
+        Z3QSO1NjbBxWnaHKZYT7nkrJm8AMCgZU0ZArFLpaufKCeiK5ECSsDxic4FIsY1OkWT42qEUfL0Wd
+        +150AKGNZpPJnnP3QYY4W/MWcKH/zdO400+zWN52WevbSqZy90tqKDJrBkMl1ydqbuw1E4ZHvIs=
+
+- job:
+    name: base
+    parent: null
+
+- job:
+    parent: base
+    name: secret-file
+    secrets:
+      - test_secret
+
+- job:
+    parent: base
+    name: secret-file-fail
+    secrets:
+      - test_secret
+
+- project:
+    name: org/project
+    check:
+      jobs: []
diff --git a/tests/fixtures/config/secret-leaks/git/org_project/README b/tests/fixtures/config/secret-leaks/git/org_project/README
new file mode 100644
index 0000000..9daeafb
--- /dev/null
+++ b/tests/fixtures/config/secret-leaks/git/org_project/README
@@ -0,0 +1 @@
+test
diff --git a/tests/fixtures/config/secret-leaks/main.yaml b/tests/fixtures/config/secret-leaks/main.yaml
new file mode 100644
index 0000000..208e274
--- /dev/null
+++ b/tests/fixtures/config/secret-leaks/main.yaml
@@ -0,0 +1,8 @@
+- tenant:
+    name: tenant-one
+    source:
+      gerrit:
+        config-projects:
+          - common-config
+        untrusted-projects:
+          - org/project
diff --git a/tests/unit/test_v3.py b/tests/unit/test_v3.py
index 5e3a6fc..2293ca0 100755
--- a/tests/unit/test_v3.py
+++ b/tests/unit/test_v3.py
@@ -1245,3 +1245,89 @@
         self.assertIn('Base jobs must be defined in config projects',
                       A.messages[0])
         self.assertHistory([])
+
+
+class TestSecretLeaks(AnsibleZuulTestCase):
+    tenant_config_file = 'config/secret-leaks/main.yaml'
+
+    def searchForContent(self, path, content):
+        matches = []
+        for (dirpath, dirnames, filenames) in os.walk(path):
+            for filename in filenames:
+                filepath = os.path.join(dirpath, filename)
+                with open(filepath, 'rb') as f:
+                    if content in f.read():
+                        matches.append(filepath[len(path):])
+        return matches
+
+    def _test_secret_file(self):
+        # Or rather -- test that they *don't* leak.
+        # Keep the jobdir around so we can inspect contents.
+        self.executor_server.keep_jobdir = True
+        conf = textwrap.dedent(
+            """
+            - project:
+                name: org/project
+                check:
+                  jobs:
+                    - secret-file
+            """)
+
+        file_dict = {'.zuul.yaml': conf}
+        A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A',
+                                           files=file_dict)
+        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+        self.waitUntilSettled()
+        self.assertHistory([
+            dict(name='secret-file', result='SUCCESS', changes='1,1'),
+        ], ordered=False)
+        matches = self.searchForContent(self.history[0].jobdir.root,
+                                        b'test-password')
+        self.assertEqual(set(['/ansible/playbook_0/secrets.yaml',
+                              '/work/secret-file.txt']),
+                         set(matches))
+
+    def test_secret_file(self):
+        self._test_secret_file()
+
+    def test_secret_file_verbose(self):
+        # Output extra ansible info to exercise alternate logging code
+        # paths.
+        self.executor_server.verbose = True
+        self._test_secret_file()
+
+    def _test_secret_file_fail(self):
+        # Or rather -- test that they *don't* leak.
+        # Keep the jobdir around so we can inspect contents.
+        self.executor_server.keep_jobdir = True
+        conf = textwrap.dedent(
+            """
+            - project:
+                name: org/project
+                check:
+                  jobs:
+                    - secret-file-fail
+            """)
+
+        file_dict = {'.zuul.yaml': conf}
+        A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A',
+                                           files=file_dict)
+        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+        self.waitUntilSettled()
+        self.assertHistory([
+            dict(name='secret-file-fail', result='FAILURE', changes='1,1'),
+        ], ordered=False)
+        matches = self.searchForContent(self.history[0].jobdir.root,
+                                        b'test-password')
+        self.assertEqual(set(['/ansible/playbook_0/secrets.yaml',
+                              '/work/failure-file.txt']),
+                         set(matches))
+
+    def test_secret_file_fail(self):
+        self._test_secret_file_fail()
+
+    def test_secret_file_fail_verbose(self):
+        # Output extra ansible info to exercise alternate logging code
+        # paths.
+        self.executor_server.verbose = True
+        self._test_secret_file_fail()