Add action plugins to restrict untrusted execution
There are actions undertaken by action plugins in normal ansible that
allow for executing code on the host that ansible is executing on. We do
not want to allow that for untrusted code, so add a set of action
plugins that override the upstream ones and simply return errors.
Additionally, we can trap for attempts to execute local commands in the
normal action plugin by looking at remote_addr, connection and
delegate_to.
Change-Id: I57dbe5648a9dc6ec9147c8698ad46c4fa1326e5a
diff --git a/tests/base.py b/tests/base.py
index 41fa29f..59cc9ae 100755
--- a/tests/base.py
+++ b/tests/base.py
@@ -757,11 +757,12 @@
self.launcher_server.lock.release()
return result
- def runAnsible(self, cmd, timeout):
+ def runAnsible(self, cmd, timeout, secure=False):
build = self.launcher_server.job_builds[self.job.unique]
if self.launcher_server._run_ansible:
- result = super(RecordingAnsibleJob, self).runAnsible(cmd, timeout)
+ result = super(RecordingAnsibleJob, self).runAnsible(
+ cmd, timeout, secure=secure)
else:
result = build.run()
return result
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 6b0af99..408810e 100644
--- a/tests/fixtures/config/ansible/git/common-config/playbooks/python27.yaml
+++ b/tests/fixtures/config/ansible/git/common-config/playbooks/python27.yaml
@@ -3,3 +3,6 @@
- file:
path: "{{zuul._test.test_root}}/{{zuul.uuid}}.flag"
state: touch
+ - copy:
+ src: "{{zuul._test.test_root}}/{{zuul.uuid}}.flag"
+ dest: "{{zuul._test.test_root}}/{{zuul.uuid}}.copied"
diff --git a/tests/fixtures/config/ansible/git/org_project/.zuul.yaml b/tests/fixtures/config/ansible/git/org_project/.zuul.yaml
index 6bedb07..6abfc47 100644
--- a/tests/fixtures/config/ansible/git/org_project/.zuul.yaml
+++ b/tests/fixtures/config/ansible/git/org_project/.zuul.yaml
@@ -1,6 +1,11 @@
+- job:
+ parent: python27
+ name: faillocal
+
- project:
name: org/project
check:
jobs:
- python27
+ - faillocal
diff --git a/tests/fixtures/config/ansible/git/org_project/playbooks/faillocal.yaml b/tests/fixtures/config/ansible/git/org_project/playbooks/faillocal.yaml
new file mode 100644
index 0000000..6689e18
--- /dev/null
+++ b/tests/fixtures/config/ansible/git/org_project/playbooks/faillocal.yaml
@@ -0,0 +1,5 @@
+- hosts: all
+ tasks:
+ - copy:
+ src: "{{zuul._test.test_root}}/{{zuul.uuid}}.flag"
+ dest: "{{zuul._test.test_root}}/{{zuul.uuid}}.failed"
diff --git a/tests/unit/test_v3.py b/tests/unit/test_v3.py
index 0ba5ff8..74d69c9 100644
--- a/tests/unit/test_v3.py
+++ b/tests/unit/test_v3.py
@@ -125,10 +125,18 @@
A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
self.waitUntilSettled()
+ build = self.getJobFromHistory('faillocal')
+ self.assertEqual(build.result, 'FAILURE')
build = self.getJobFromHistory('python27')
self.assertEqual(build.result, 'SUCCESS')
flag_path = os.path.join(self.test_root, build.uuid + '.flag')
self.assertTrue(os.path.exists(flag_path))
+ copied_path = os.path.join(self.test_root, build.uuid +
+ '.copied')
+ self.assertTrue(os.path.exists(copied_path))
+ failed_path = os.path.join(self.test_root, build.uuid +
+ '.failed')
+ self.assertFalse(os.path.exists(failed_path))
pre_flag_path = os.path.join(self.test_root, build.uuid +
'.pre.flag')
self.assertTrue(os.path.exists(pre_flag_path))
diff --git a/zuul/ansible/action/__init__.py b/zuul/ansible/action/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/zuul/ansible/action/__init__.py
diff --git a/zuul/ansible/action/add_host.py b/zuul/ansible/action/add_host.py
new file mode 100644
index 0000000..e41e4e1
--- /dev/null
+++ b/zuul/ansible/action/add_host.py
@@ -0,0 +1,25 @@
+# Copyright 2016 Red Hat, Inc.
+#
+# This module is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This software is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this software. If not, see <http://www.gnu.org/licenses/>.
+
+from zuul.ansible.plugins.action import add_host
+
+
+class ActionModule(add_host.ActionModule):
+
+ def run(self, tmp=None, task_vars=None):
+
+ return dict(
+ failed=True,
+ msg="Adding hosts to the inventory is prohibited")
diff --git a/zuul/ansible/action/asa_config.py b/zuul/ansible/action/asa_config.py
new file mode 120000
index 0000000..7a739ba
--- /dev/null
+++ b/zuul/ansible/action/asa_config.py
@@ -0,0 +1 @@
+network.py
\ No newline at end of file
diff --git a/zuul/ansible/action/asa_template.py b/zuul/ansible/action/asa_template.py
new file mode 120000
index 0000000..7a739ba
--- /dev/null
+++ b/zuul/ansible/action/asa_template.py
@@ -0,0 +1 @@
+network.py
\ No newline at end of file
diff --git a/zuul/ansible/action/assemble.py b/zuul/ansible/action/assemble.py
new file mode 100644
index 0000000..d0bff37
--- /dev/null
+++ b/zuul/ansible/action/assemble.py
@@ -0,0 +1,30 @@
+# Copyright 2016 Red Hat, Inc.
+#
+# This module is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This software is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this software. If not, see <http://www.gnu.org/licenses/>.
+
+
+from zuul.ansible import paths
+from zuul.ansible.plugins.action import assemble
+
+
+class ActionModule(assemble.ActionModule):
+
+ def run(self, tmp=None, task_vars=None):
+
+ source = self._task.args.get('src', None)
+ remote_src = self._task.args.get('remote_src', False)
+
+ if not remote_src and not paths._is_safe_path(source):
+ return paths._fail_dict(source)
+ return super(ActionModule, self).run(tmp, task_vars)
diff --git a/zuul/ansible/action/copy.py b/zuul/ansible/action/copy.py
new file mode 100644
index 0000000..5dc9fa8
--- /dev/null
+++ b/zuul/ansible/action/copy.py
@@ -0,0 +1,30 @@
+# Copyright 2016 Red Hat, Inc.
+#
+# This module is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This software is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this software. If not, see <http://www.gnu.org/licenses/>.
+
+
+from zuul.ansible import paths
+from zuul.ansible.plugins.action import copy
+
+
+class ActionModule(copy.ActionModule):
+
+ def run(self, tmp=None, task_vars=None):
+
+ source = self._task.args.get('src', None)
+ remote_src = self._task.args.get('remote_src', False)
+
+ if not remote_src and not paths._is_safe_path(source):
+ return paths._fail_dict(source)
+ return super(ActionModule, self).run(tmp, task_vars)
diff --git a/zuul/ansible/action/dellos10_config.py b/zuul/ansible/action/dellos10_config.py
new file mode 120000
index 0000000..7a739ba
--- /dev/null
+++ b/zuul/ansible/action/dellos10_config.py
@@ -0,0 +1 @@
+network.py
\ No newline at end of file
diff --git a/zuul/ansible/action/dellos6_config.py b/zuul/ansible/action/dellos6_config.py
new file mode 120000
index 0000000..7a739ba
--- /dev/null
+++ b/zuul/ansible/action/dellos6_config.py
@@ -0,0 +1 @@
+network.py
\ No newline at end of file
diff --git a/zuul/ansible/action/dellos9_config.py b/zuul/ansible/action/dellos9_config.py
new file mode 120000
index 0000000..7a739ba
--- /dev/null
+++ b/zuul/ansible/action/dellos9_config.py
@@ -0,0 +1 @@
+network.py
\ No newline at end of file
diff --git a/zuul/ansible/action/eos_config.py b/zuul/ansible/action/eos_config.py
new file mode 120000
index 0000000..7a739ba
--- /dev/null
+++ b/zuul/ansible/action/eos_config.py
@@ -0,0 +1 @@
+network.py
\ No newline at end of file
diff --git a/zuul/ansible/action/eos_template.py b/zuul/ansible/action/eos_template.py
new file mode 120000
index 0000000..7a739ba
--- /dev/null
+++ b/zuul/ansible/action/eos_template.py
@@ -0,0 +1 @@
+network.py
\ No newline at end of file
diff --git a/zuul/ansible/action/fetch.py b/zuul/ansible/action/fetch.py
new file mode 100644
index 0000000..fe06c3b
--- /dev/null
+++ b/zuul/ansible/action/fetch.py
@@ -0,0 +1,29 @@
+# Copyright 2016 Red Hat, Inc.
+#
+# This module is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This software is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this software. If not, see <http://www.gnu.org/licenses/>.
+
+
+from zuul.ansible import paths
+from zuul.ansible.plugins.action import fetch
+
+
+class ActionModule(fetch.ActionModule):
+
+ def run(self, tmp=None, task_vars=None):
+
+ dest = self._task.args.get('dest', None)
+
+ if dest and not paths._is_safe_path(dest):
+ return paths._fail_dict(dest)
+ return super(ActionModule, self).run(tmp, task_vars)
diff --git a/zuul/ansible/action/include_vars.py b/zuul/ansible/action/include_vars.py
new file mode 100644
index 0000000..aa0e7d8
--- /dev/null
+++ b/zuul/ansible/action/include_vars.py
@@ -0,0 +1,31 @@
+# Copyright 2016 Red Hat, Inc.
+#
+# This module is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This software is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this software. If not, see <http://www.gnu.org/licenses/>.
+
+
+from zuul.ansible import paths
+from zuul.ansible.plugins.action import include_vars
+
+
+class ActionModule(include_vars.ActionModule):
+
+ def run(self, tmp=None, task_vars=None):
+
+ source_dir = self._task.args.get('dir', None)
+ source_file = self._task.args.get('file', None)
+
+ for fileloc in (source_dir, source_file):
+ if fileloc and not paths._is_safe_path(fileloc):
+ return paths._fail_dict(fileloc)
+ return super(ActionModule, self).run(tmp, task_vars)
diff --git a/zuul/ansible/action/ios_config.py b/zuul/ansible/action/ios_config.py
new file mode 120000
index 0000000..7a739ba
--- /dev/null
+++ b/zuul/ansible/action/ios_config.py
@@ -0,0 +1 @@
+network.py
\ No newline at end of file
diff --git a/zuul/ansible/action/ios_template.py b/zuul/ansible/action/ios_template.py
new file mode 120000
index 0000000..7a739ba
--- /dev/null
+++ b/zuul/ansible/action/ios_template.py
@@ -0,0 +1 @@
+network.py
\ No newline at end of file
diff --git a/zuul/ansible/action/iosxr_config.py b/zuul/ansible/action/iosxr_config.py
new file mode 120000
index 0000000..7a739ba
--- /dev/null
+++ b/zuul/ansible/action/iosxr_config.py
@@ -0,0 +1 @@
+network.py
\ No newline at end of file
diff --git a/zuul/ansible/action/iosxr_template.py b/zuul/ansible/action/iosxr_template.py
new file mode 120000
index 0000000..7a739ba
--- /dev/null
+++ b/zuul/ansible/action/iosxr_template.py
@@ -0,0 +1 @@
+network.py
\ No newline at end of file
diff --git a/zuul/ansible/action/junos_config.py b/zuul/ansible/action/junos_config.py
new file mode 120000
index 0000000..7a739ba
--- /dev/null
+++ b/zuul/ansible/action/junos_config.py
@@ -0,0 +1 @@
+network.py
\ No newline at end of file
diff --git a/zuul/ansible/action/junos_template.py b/zuul/ansible/action/junos_template.py
new file mode 120000
index 0000000..7a739ba
--- /dev/null
+++ b/zuul/ansible/action/junos_template.py
@@ -0,0 +1 @@
+network.py
\ No newline at end of file
diff --git a/zuul/ansible/action/net_config.py b/zuul/ansible/action/net_config.py
new file mode 120000
index 0000000..7a739ba
--- /dev/null
+++ b/zuul/ansible/action/net_config.py
@@ -0,0 +1 @@
+network.py
\ No newline at end of file
diff --git a/zuul/ansible/action/net_template.py b/zuul/ansible/action/net_template.py
new file mode 120000
index 0000000..7a739ba
--- /dev/null
+++ b/zuul/ansible/action/net_template.py
@@ -0,0 +1 @@
+network.py
\ No newline at end of file
diff --git a/zuul/ansible/action/network.py b/zuul/ansible/action/network.py
new file mode 100644
index 0000000..31a8739
--- /dev/null
+++ b/zuul/ansible/action/network.py
@@ -0,0 +1,24 @@
+# Copyright 2016 Red Hat, Inc.
+#
+# This module is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This software is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this software. If not, see <http://www.gnu.org/licenses/>.
+
+
+from zuul.ansible.plugins.action import network
+
+
+class ActionModule(network.ActionModule):
+
+ def run(self, tmp=None, task_vars=None):
+
+ return dict(failed=True, msg='Use of network modules is prohibited')
diff --git a/zuul/ansible/action/normal.py b/zuul/ansible/action/normal.py
new file mode 100644
index 0000000..d4b2396
--- /dev/null
+++ b/zuul/ansible/action/normal.py
@@ -0,0 +1,33 @@
+# Copyright 2016 Red Hat, Inc.
+#
+# This module is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This software is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this software. If not, see <http://www.gnu.org/licenses/>.
+
+
+from zuul.ansible.plugins.action import normal
+
+
+class ActionModule(normal.ActionModule):
+
+ def run(self, tmp=None, task_vars=None):
+
+ if (self._play_context.connection == 'local'
+ or self._play_context.remote_addr == 'localhost'
+ or self._play_context.remote_addr.startswith('127.')
+ or self._task.delegate_to == 'localhost'
+ or (self._task.delegate_to
+ and self._task.delegate_to.startswtih('127.'))):
+ return dict(
+ failed=True,
+ msg="Executing local code is prohibited")
+ return super(ActionModule, self).run(tmp, task_vars)
diff --git a/zuul/ansible/action/nxos_config.py b/zuul/ansible/action/nxos_config.py
new file mode 120000
index 0000000..7a739ba
--- /dev/null
+++ b/zuul/ansible/action/nxos_config.py
@@ -0,0 +1 @@
+network.py
\ No newline at end of file
diff --git a/zuul/ansible/action/nxos_template.py b/zuul/ansible/action/nxos_template.py
new file mode 120000
index 0000000..7a739ba
--- /dev/null
+++ b/zuul/ansible/action/nxos_template.py
@@ -0,0 +1 @@
+network.py
\ No newline at end of file
diff --git a/zuul/ansible/action/ops_config.py b/zuul/ansible/action/ops_config.py
new file mode 120000
index 0000000..7a739ba
--- /dev/null
+++ b/zuul/ansible/action/ops_config.py
@@ -0,0 +1 @@
+network.py
\ No newline at end of file
diff --git a/zuul/ansible/action/ops_template.py b/zuul/ansible/action/ops_template.py
new file mode 120000
index 0000000..7a739ba
--- /dev/null
+++ b/zuul/ansible/action/ops_template.py
@@ -0,0 +1 @@
+network.py
\ No newline at end of file
diff --git a/zuul/ansible/action/patch.py b/zuul/ansible/action/patch.py
new file mode 100644
index 0000000..d630844
--- /dev/null
+++ b/zuul/ansible/action/patch.py
@@ -0,0 +1,30 @@
+# Copyright 2016 Red Hat, Inc.
+#
+# This module is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This software is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this software. If not, see <http://www.gnu.org/licenses/>.
+
+
+from zuul.ansible import paths
+from zuul.ansible.plugins.action import patch
+
+
+class ActionModule(patch.ActionModule):
+
+ def run(self, tmp=None, task_vars=None):
+
+ source = self._task.args.get('src', None)
+ remote_src = self._task.args.get('remote_src', False)
+
+ if not remote_src and not paths._is_safe_path(source):
+ return paths._fail_dict(source)
+ return super(ActionModule, self).run(tmp, task_vars)
diff --git a/zuul/ansible/action/script.py b/zuul/ansible/action/script.py
new file mode 100644
index 0000000..bd3d5d5
--- /dev/null
+++ b/zuul/ansible/action/script.py
@@ -0,0 +1,34 @@
+# Copyright 2016 Red Hat, Inc.
+#
+# This module is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This software is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this software. If not, see <http://www.gnu.org/licenses/>.
+
+
+from zuul.ansible import paths
+from zuul.ansible.plugins.action import copy
+
+
+class ActionModule(copy.ActionModule):
+
+ def run(self, tmp=None, task_vars=None):
+
+ # the script name is the first item in the raw params, so we split it
+ # out now so we know the file name we need to transfer to the remote,
+ # and everything else is an argument to the script which we need later
+ # to append to the remote command
+ parts = self._task.args.get('_raw_params', '').strip().split()
+ source = parts[0]
+
+ if not paths._is_safe_path(source):
+ return paths._fail_dict(source)
+ return super(ActionModule, self).run(tmp, task_vars)
diff --git a/zuul/ansible/action/sros_config.py b/zuul/ansible/action/sros_config.py
new file mode 120000
index 0000000..7a739ba
--- /dev/null
+++ b/zuul/ansible/action/sros_config.py
@@ -0,0 +1 @@
+network.py
\ No newline at end of file
diff --git a/zuul/ansible/action/synchronize.py b/zuul/ansible/action/synchronize.py
new file mode 100644
index 0000000..cbb7ea2
--- /dev/null
+++ b/zuul/ansible/action/synchronize.py
@@ -0,0 +1,33 @@
+# Copyright 2016 Red Hat, Inc.
+#
+# This module is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This software is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this software. If not, see <http://www.gnu.org/licenses/>.
+
+
+from zuul.ansible import paths
+from zuul.ansible.plugins.action import synchronize
+
+
+class ActionModule(synchronize.ActionModule):
+
+ def run(self, tmp=None, task_vars=None):
+
+ source = self._task.args.get('src', None)
+ dest = self._task.args.get('dest', None)
+ pull = self._task.args.get('pull', False)
+
+ if not pull and not paths._is_safe_path(source):
+ return paths._fail_dict(source, prefix='Syncing files from')
+ if pull and not paths._is_safe_path(dest):
+ return paths._fail_dict(dest, prefix='Syncing files to')
+ return super(ActionModule, self).run(tmp, task_vars)
diff --git a/zuul/ansible/action/template.py b/zuul/ansible/action/template.py
new file mode 100644
index 0000000..96471ae
--- /dev/null
+++ b/zuul/ansible/action/template.py
@@ -0,0 +1,29 @@
+# Copyright 2016 Red Hat, Inc.
+#
+# This module is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This software is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this software. If not, see <http://www.gnu.org/licenses/>.
+
+
+from zuul.ansible import paths
+from zuul.ansible.plugins.action import template
+
+
+class ActionModule(template.ActionModule):
+
+ def run(self, tmp=None, task_vars=None):
+
+ source = self._task.args.get('src', None)
+
+ if not paths._is_safe_path(source):
+ return paths._fail_dict(source)
+ return super(ActionModule, self).run(tmp, task_vars)
diff --git a/zuul/ansible/action/unarchive.py b/zuul/ansible/action/unarchive.py
new file mode 100644
index 0000000..c3f6e91
--- /dev/null
+++ b/zuul/ansible/action/unarchive.py
@@ -0,0 +1,30 @@
+# Copyright 2016 Red Hat, Inc.
+#
+# This module is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This software is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this software. If not, see <http://www.gnu.org/licenses/>.
+
+
+from zuul.ansible import paths
+from zuul.ansible.plugins.action import unarchive
+
+
+class ActionModule(unarchive.ActionModule):
+
+ def run(self, tmp=None, task_vars=None):
+
+ source = self._task.args.get('src', None)
+ remote_src = self._task.args.get('remote_src', False)
+
+ if not remote_src and not paths._is_safe_path(source):
+ return paths._fail_dict(source)
+ return super(ActionModule, self).run(tmp, task_vars)
diff --git a/zuul/ansible/action/vyos_config.py b/zuul/ansible/action/vyos_config.py
new file mode 120000
index 0000000..7a739ba
--- /dev/null
+++ b/zuul/ansible/action/vyos_config.py
@@ -0,0 +1 @@
+network.py
\ No newline at end of file
diff --git a/zuul/ansible/action/win_copy.py b/zuul/ansible/action/win_copy.py
new file mode 100644
index 0000000..eef3a1c
--- /dev/null
+++ b/zuul/ansible/action/win_copy.py
@@ -0,0 +1,30 @@
+# Copyright 2016 Red Hat, Inc.
+#
+# This module is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This software is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this software. If not, see <http://www.gnu.org/licenses/>.
+
+
+from zuul.ansible import paths
+from zuul.ansible.plugins.action import win_copy
+
+
+class ActionModule(win_copy.ActionModule):
+
+ def run(self, tmp=None, task_vars=None):
+
+ source = self._task.args.get('src', None)
+ remote_src = self._task.args.get('remote_src', False)
+
+ if not remote_src and not paths._is_safe_path(source):
+ return paths._fail_dict(source)
+ return super(ActionModule, self).run(tmp, task_vars)
diff --git a/zuul/ansible/action/win_template.py b/zuul/ansible/action/win_template.py
new file mode 100644
index 0000000..2a47216
--- /dev/null
+++ b/zuul/ansible/action/win_template.py
@@ -0,0 +1,30 @@
+# Copyright 2016 Red Hat, Inc.
+#
+# This module is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This software is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this software. If not, see <http://www.gnu.org/licenses/>.
+
+
+from zuul.ansible import paths
+from zuul.ansible.plugins.action import win_template
+
+
+class ActionModule(win_template.ActionModule):
+
+ def run(self, tmp=None, task_vars=None):
+
+ source = self._task.args.get('src', None)
+ remote_src = self._task.args.get('remote_src', False)
+
+ if not remote_src and not paths._is_safe_path(source):
+ return paths._fail_dict(source)
+ return super(ActionModule, self).run(tmp, task_vars)
diff --git a/zuul/ansible/paths.py b/zuul/ansible/paths.py
new file mode 100644
index 0000000..2bd0181
--- /dev/null
+++ b/zuul/ansible/paths.py
@@ -0,0 +1,33 @@
+# Copyright 2016 Red Hat, Inc.
+#
+# This module is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This software is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this software. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+
+def _is_safe_path(path):
+ if os.path.isabs(path):
+ return False
+ if not os.path.abspath(os.path.expanduser(path)).startswith(
+ os.path.abspath(os.path.curdir)):
+ return False
+ return True
+
+
+def _fail_dict(path, prefix='Accessing files from'):
+ return dict(
+ failed=True,
+ path=path,
+ msg="{prefix} outside the working dir is prohibited".format(
+ prefix=prefix))
diff --git a/zuul/launcher/server.py b/zuul/launcher/server.py
index df71cc9..ce0ced6 100644
--- a/zuul/launcher/server.py
+++ b/zuul/launcher/server.py
@@ -29,6 +29,7 @@
import gear
import zuul.merger.merger
+import zuul.ansible.action
import zuul.ansible.library
from zuul.lib import commandsocket
@@ -85,6 +86,8 @@
os.makedirs(self.git_root)
self.ansible_root = os.path.join(self.root, 'ansible')
os.makedirs(self.ansible_root)
+ self.secure_ansible_root = os.path.join(self.ansible_root, 'secure')
+ os.makedirs(self.secure_ansible_root)
self.known_hosts = os.path.join(self.ansible_root, 'known_hosts')
self.inventory = os.path.join(self.ansible_root, 'inventory')
self.vars = os.path.join(self.ansible_root, 'vars.yaml')
@@ -93,6 +96,8 @@
self.pre_playbooks = []
self.post_playbooks = []
self.config = os.path.join(self.ansible_root, 'ansible.cfg')
+ self.secure_config = os.path.join(
+ self.secure_ansible_root, 'ansible.cfg')
self.ansible_log = os.path.join(self.ansible_root, 'ansible_log.txt')
def addPrePlaybook(self):
@@ -238,11 +243,18 @@
self.library_dir = os.path.join(ansible_dir, 'library')
if not os.path.exists(self.library_dir):
os.makedirs(self.library_dir)
+ self.action_dir = os.path.join(ansible_dir, 'action')
+ if not os.path.exists(self.action_dir):
+ os.makedirs(self.action_dir)
library_path = os.path.dirname(os.path.abspath(
zuul.ansible.library.__file__))
for fn in os.listdir(library_path):
shutil.copy(os.path.join(library_path, fn), self.library_dir)
+ action_path = os.path.dirname(os.path.abspath(
+ zuul.ansible.action.__file__))
+ for fn in os.listdir(action_path):
+ shutil.copy(os.path.join(action_path, fn), self.action_dir)
self.job_workers = {}
@@ -580,10 +592,28 @@
hosts.append((node['name'], dict(ansible_connection='local')))
return hosts
- def findPlaybook(self, path, required=False):
+ def _blockPluginDirs(self, fn):
+ '''Prevent execution of playbooks with plugins
+
+ Plugins are loaded from roles and also if there is a plugin dir
+ adjacent to the playbook. Role exclusion will be handled elsewhere,
+ but while we're looking for playbooks, throw an error if the playbook
+ exists in a location that would cause a plugin to get loaded if the
+ playbook is not in a secure repository.
+ '''
+ playbook_dir = os.path.dirname(os.path.abspath(fn))
+ for entry in os.listdir(playbook_dir):
+ if os.path.isdir(entry) and entry.endswith('_plugins'):
+ raise Exception(
+ "Ansible plugin dir %s found adjacent to playbook %s in"
+ " non-secure repo." % (entry, fn))
+
+ def findPlaybook(self, path, required=False, secure=False):
for ext in ['.yaml', '.yml']:
fn = path + ext
if os.path.exists(fn):
+ if not secure:
+ self._blockPluginDirs(fn)
return fn
if required:
raise Exception("Unable to find playbook %s" % path)
@@ -631,7 +661,10 @@
path = os.path.join(self.jobdir.git_root,
project.name,
playbook['path'])
- jobdir_playbook.path = self.findPlaybook(path, main)
+ jobdir_playbook.path = self.findPlaybook(
+ path,
+ required=main,
+ secure=playbook['secure'])
return
# The playbook repo is either a config repo, or it isn't in
# the stack of changes we are testing, so check out the branch
@@ -643,7 +676,10 @@
path = os.path.join(jobdir_playbook.root,
project.name,
playbook['path'])
- jobdir_playbook.path = self.findPlaybook(path, main)
+ jobdir_playbook.path = self.findPlaybook(
+ path,
+ required=main,
+ secure=playbook['secure'])
def prepareAnsibleFiles(self, args):
with open(self.jobdir.inventory, 'w') as inventory:
@@ -657,7 +693,11 @@
zuul_vars = dict(zuul=args['zuul'])
vars_yaml.write(
yaml.safe_dump(zuul_vars, default_flow_style=False))
- with open(self.jobdir.config, 'w') as config:
+ self.writeAnsibleConfig(self.jobdir.config)
+ self.writeAnsibleConfig(self.jobdir.secure_config, secure=True)
+
+ def writeAnsibleConfig(self, config_path, secure=False):
+ with open(config_path, 'w') as config:
config.write('[defaults]\n')
config.write('hostfile = %s\n' % self.jobdir.inventory)
config.write('local_tmp = %s/.ansible/local_tmp\n' %
@@ -673,6 +713,9 @@
# bump the timeout because busy nodes may take more than
# 10s to respond
config.write('timeout = 30\n')
+ if not secure:
+ config.write('action_plugins = %s\n'
+ % self.launcher_server.action_dir)
config.write('[ssh_connection]\n')
# NB: when setting pipelining = True, keep_remote_files
@@ -706,17 +749,22 @@
self.log.exception("Exception while killing "
"ansible process:")
- def runAnsible(self, cmd, timeout):
+ def runAnsible(self, cmd, timeout, secure=False):
env_copy = os.environ.copy()
env_copy['LOGNAME'] = 'zuul'
+ if secure:
+ cwd = self.jobdir.secure_ansible_root
+ else:
+ cwd = self.jobdir.ansible_root
+
with self.proc_lock:
if self.aborted:
return (self.RESULT_ABORTED, None)
self.log.debug("Ansible command: %s" % (cmd,))
self.proc = subprocess.Popen(
cmd,
- cwd=self.jobdir.ansible_root,
+ cwd=cwd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
preexec_fn=os.setsid,
@@ -771,4 +819,5 @@
# TODOv3: get this from the job
timeout = 60
- return self.runAnsible(cmd, timeout)
+ return self.runAnsible(
+ cmd=cmd, timeout=timeout, secure=playbook.secure)