Decrypt secrets and plumb to Ansible

When configuring jobs, decrypt secrets they reference.  Pass the
resulting values to ansible variables.

Change-Id: Ibe2b6b84fdc0f4287e4dc1681218df2228f92db0
diff --git a/tests/fixtures/config/ansible/git/common-config/playbooks/python27.yaml b/tests/fixtures/config/ansible/git/common-config/playbooks/python27.yaml
index 45acb87..3371a20 100644
--- a/tests/fixtures/config/ansible/git/common-config/playbooks/python27.yaml
+++ b/tests/fixtures/config/ansible/git/common-config/playbooks/python27.yaml
@@ -6,5 +6,8 @@
     - copy:
         src: "{{zuul._test.test_root}}/{{zuul.uuid}}.flag"
         dest: "{{zuul._test.test_root}}/{{zuul.uuid}}.copied"
+    - copy:
+        content: "{{test_secret.username}} {{test_secret.password}}"
+        dest: "{{zuul._test.test_root}}/{{zuul.uuid}}.secrets"
   roles:
     - bare-role
diff --git a/tests/fixtures/config/ansible/git/common-config/zuul.yaml b/tests/fixtures/config/ansible/git/common-config/zuul.yaml
index eb3dbd8..c21d694 100644
--- a/tests/fixtures/config/ansible/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/ansible/git/common-config/zuul.yaml
@@ -57,6 +57,9 @@
       flagpath: '{{zuul._test.test_root}}/{{zuul.uuid}}.flag'
     roles:
       - zuul: bare-role
+    auth:
+      secrets:
+        - test_secret
 
 - job:
     parent: python27
diff --git a/tests/unit/test_model.py b/tests/unit/test_model.py
index 57ee54b..d2da426 100644
--- a/tests/unit/test_model.py
+++ b/tests/unit/test_model.py
@@ -19,11 +19,13 @@
 import fixtures
 import testtools
 import yaml
+from cryptography.hazmat.primitives import serialization
+from cryptography.hazmat.backends import default_backend
 
 from zuul import model
 from zuul import configloader
 
-from tests.base import BaseTestCase
+from tests.base import BaseTestCase, FIXTURE_DIR
 
 
 class TestJob(BaseTestCase):
@@ -31,6 +33,13 @@
     def setUp(self):
         super(TestJob, self).setUp()
         self.project = model.Project('project', None)
+        private_key_file = os.path.join(FIXTURE_DIR, 'private.pem')
+        with open(private_key_file, "rb") as f:
+            self.project.private_key = serialization.load_pem_private_key(
+                f.read(),
+                password=None,
+                backend=default_backend()
+            )
         self.context = model.SourceContext(self.project, 'master',
                                            'test', True)
         self.start_mark = yaml.Mark('name', 0, 0, 0, '', 0)
diff --git a/tests/unit/test_v3.py b/tests/unit/test_v3.py
index 52b4177..97816ea 100644
--- a/tests/unit/test_v3.py
+++ b/tests/unit/test_v3.py
@@ -290,6 +290,11 @@
                                            build.uuid + '.bare-role.flag')
         self.assertTrue(os.path.exists(bare_role_flag_path))
 
+        secrets_path = os.path.join(self.test_root,
+                                    build.uuid + '.secrets')
+        with open(secrets_path) as f:
+            self.assertEqual(f.read(), "test-username test-password")
+
 
 class TestBrokenConfig(ZuulTestCase):
     # Test that we get an appropriate syntax error if we start with a
diff --git a/zuul/configloader.py b/zuul/configloader.py
index 22ba3fb..cbd7d40 100644
--- a/zuul/configloader.py
+++ b/zuul/configloader.py
@@ -24,6 +24,8 @@
 from cryptography.hazmat.backends import default_backend
 from cryptography.hazmat.primitives.asymmetric import rsa
 from cryptography.hazmat.primitives import serialization
+from cryptography.hazmat.primitives.asymmetric import padding
+from cryptography.hazmat.primitives import hashes
 from zuul import model
 import zuul.manager.dependent
 import zuul.manager.independent
@@ -130,12 +132,32 @@
     yaml_loader = yaml.SafeLoader
 
     def __init__(self, ciphertext):
-        self.ciphertext = ciphertext
+        self.ciphertext = ciphertext.decode('base64')
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def __eq__(self, other):
+        if not isinstance(other, EncryptedPKCS1):
+            return False
+        return (self.ciphertext == other.ciphertext)
 
     @classmethod
     def from_yaml(cls, loader, node):
         return cls(node.value)
 
+    def decrypt(self, private_key):
+        # https://cryptography.io/en/latest/hazmat/primitives/asymmetric/rsa/#decryption
+        plaintext = private_key.decrypt(
+            self.ciphertext,
+            padding.OAEP(
+                mgf=padding.MGF1(algorithm=hashes.SHA1()),
+                algorithm=hashes.SHA1(),
+                label=None
+            )
+        )
+        return plaintext
+
 
 class NodeSetParser(object):
     @staticmethod
@@ -272,7 +294,8 @@
                         "Unable to use secret %s.  Secrets must be "
                         "defined in the same project in which they "
                         "are used" % secret_name)
-                job.auth.secrets.append(secret)
+                job.auth.secrets.append(secret.decrypt(
+                    job.source_context.project.private_key))
 
         if 'parent' in conf:
             parent = layout.getJob(conf['parent'])
diff --git a/zuul/executor/client.py b/zuul/executor/client.py
index 31646f8..9b39e78 100644
--- a/zuul/executor/client.py
+++ b/zuul/executor/client.py
@@ -318,6 +318,9 @@
                               public_ipv4=node.public_ipv4))
         params['nodes'] = nodes
         params['vars'] = copy.deepcopy(job.variables)
+        if job.auth:
+            for secret in job.auth.secrets:
+                params['vars'][secret.name] = copy.deepcopy(secret.secret_data)
         params['vars']['zuul'] = zuul_params
         projects = set()
         if job.repos:
diff --git a/zuul/model.py b/zuul/model.py
index 832f0dd..47f20ef 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -545,6 +545,20 @@
     def __repr__(self):
         return '<Secret %s>' % (self.name,)
 
+    def decrypt(self, private_key):
+        """Return a copy of this secret with any encrypted data decrypted.
+        Note that the original remains encrypted."""
+
+        r = copy.deepcopy(self)
+        decrypted_secret_data = {}
+        for k, v in r.secret_data.items():
+            if hasattr(v, 'decrypt'):
+                decrypted_secret_data[k] = v.decrypt(private_key)
+            else:
+                decrypted_secret_data[k] = v
+        r.secret_data = decrypted_secret_data
+        return r
+
 
 class SourceContext(object):
     """A reference to the branch of a project in configuration.