From 0718b1bf4af69712d18f6ea3a427c1cab2e377da Mon Sep 17 00:00:00 2001 From: Jakub Hrozek Date: Mon, 8 Aug 2016 17:49:05 +0200 Subject: [PATCH 68/79] TESTS: Add integration tests for the sssd-secrets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements a simple HTTP client and uses it to talk to the sssd-secrets responder. Only the local provider is tested at the moment. Resolves: https://fedorahosted.org/sssd/ticket/3054 Reviewed-by: Petr Čech Reviewed-by: Lukáš Slebodník (cherry picked from commit db0982c52294ee5ea08ed242d27660783fde29cd) --- contrib/ci/deps.sh | 2 + src/tests/intg/Makefile.am | 5 ++ src/tests/intg/config.py.m4 | 3 + src/tests/intg/secrets.py | 137 ++++++++++++++++++++++++++++++++++ src/tests/intg/test_secrets.py | 162 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 309 insertions(+) create mode 100644 src/tests/intg/secrets.py create mode 100644 src/tests/intg/test_secrets.py diff --git a/contrib/ci/deps.sh b/contrib/ci/deps.sh index 1a94e3df2ee1d43dd34ef8cda1542aab1166bccd..9a7098c399df319753858a4a7fee23d4204c1f1c 100644 --- a/contrib/ci/deps.sh +++ b/contrib/ci/deps.sh @@ -45,6 +45,7 @@ if [[ "$DISTRO_BRANCH" == -redhat-* ]]; then pyldb rpm-build uid_wrapper + python-requests ) _DEPS_LIST_SPEC=` sed -e 's/@PACKAGE_VERSION@/0/g' \ @@ -114,6 +115,7 @@ if [[ "$DISTRO_BRANCH" == -debian-* ]]; then python-pytest python-ldap python-ldb + python-requests ldap-utils slapd systemtap-sdt-dev diff --git a/src/tests/intg/Makefile.am b/src/tests/intg/Makefile.am index 75422a4417046116bec11a8a680fe2248e3afb69..1e08eadcbbdebcca6f0f3550cc084c1a1762c0c4 100644 --- a/src/tests/intg/Makefile.am +++ b/src/tests/intg/Makefile.am @@ -16,6 +16,8 @@ dist_noinst_DATA = \ test_memory_cache.py \ test_ts_cache.py \ test_netgroup.py \ + secrets.py \ + test_secrets.py \ $(NULL) config.py: config.py.m4 @@ -25,6 +27,9 @@ config.py: config.py.m4 -D "pidpath=\`$(pidpath)'" \ -D "logpath=\`$(logpath)'" \ -D "mcpath=\`$(mcpath)'" \ + -D "secdbpath=\`$(secdbpath)'" \ + -D "libexecpath=\`$(libexecdir)'" \ + -D "runstatedir=\`$(runstatedir)'" \ $< > $@ root: diff --git a/src/tests/intg/config.py.m4 b/src/tests/intg/config.py.m4 index 77aa47b7958783217132b724159d9d3d247e1079..65e17e55a25372754ff7e49ac75607bcc985912c 100644 --- a/src/tests/intg/config.py.m4 +++ b/src/tests/intg/config.py.m4 @@ -12,3 +12,6 @@ PID_PATH = "pidpath" PIDFILE_PATH = PID_PATH + "/sssd.pid" LOG_PATH = "logpath" MCACHE_PATH = "mcpath" +SECDB_PATH = "secdbpath" +LIBEXEC_PATH = "libexecpath" +RUNSTATEDIR = "runstatedir" diff --git a/src/tests/intg/secrets.py b/src/tests/intg/secrets.py new file mode 100644 index 0000000000000000000000000000000000000000..5d4c0e2f28db9601fa0e3a21dd90a7444c7c8978 --- /dev/null +++ b/src/tests/intg/secrets.py @@ -0,0 +1,137 @@ +# +# Secrets responder test client +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This 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; version 2 only +# +# This program 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 program. If not, see . +# + +import socket +import requests + +from requests.adapters import HTTPAdapter +from requests.packages.urllib3.connection import HTTPConnection +from requests.packages.urllib3.connectionpool import HTTPConnectionPool +from requests.compat import quote, unquote, urlparse + + +class HTTPUnixConnection(HTTPConnection): + def __init__(self, host, timeout=60, **kwargs): + super(HTTPUnixConnection, self).__init__('localhost') + self.unix_socket = host + self.timeout = timeout + + def connect(self): + sock = socket.socket(family=socket.AF_UNIX) + sock.settimeout(self.timeout) + sock.connect(self.unix_socket) + self.sock = sock + + +class HTTPUnixConnectionPool(HTTPConnectionPool): + scheme = 'http+unix' + ConnectionCls = HTTPUnixConnection + + +class HTTPUnixAdapter(HTTPAdapter): + def get_connection(self, url, proxies=None): + # proxies, silently ignored + path = unquote(urlparse(url).netloc) + return HTTPUnixConnectionPool(path) + + +class SecretsHttpClient(object): + secrets_sock_path = '/var/run/secrets.socket' + secrets_container = 'secrets' + + def __init__(self, content_type='application/json', sock_path=None): + if sock_path is None: + sock_path = self.secrets_sock_path + + self.content_type = content_type + self.session = requests.Session() + self.session.mount('http+unix://', HTTPUnixAdapter()) + self.headers = dict({'Content-Type': content_type}) + self.url = 'http+unix://' + \ + quote(sock_path, safe='') + \ + '/' + \ + self.secrets_container + self._last_response = None + + def _join_url(self, resource): + path = self.url.rstrip('/') + '/' + if resource is not None: + path = path + resource.lstrip('/') + return path + + def _add_headers(self, **kwargs): + headers = kwargs.get('headers', None) + if headers is None: + headers = dict() + headers.update(self.headers) + return headers + + def _request(self, cmd, path, **kwargs): + self._last_response = None + url = self._join_url(path) + kwargs['headers'] = self._add_headers(**kwargs) + self._last_response = cmd(url, **kwargs) + return self._last_response + + @property + def last_response(self): + return self._last_response + + def get(self, path, **kwargs): + return self._request(self.session.get, path, **kwargs) + + def list(self, **kwargs): + return self._request(self.session.get, None, **kwargs) + + def put(self, name, **kwargs): + return self._request(self.session.put, name, **kwargs) + + def delete(self, name, **kwargs): + return self._request(self.session.delete, name, **kwargs) + + def post(self, name, **kwargs): + return self._request(self.session.post, name, **kwargs) + + +class SecretsLocalClient(SecretsHttpClient): + def list_secrets(self): + res = self.list() + res.raise_for_status() + simple = res.json() + return simple + + def get_secret(self, name): + res = self.get(name) + res.raise_for_status() + simple = res.json() + ktype = simple.get("type", None) + if ktype != "simple": + raise TypeError("Invalid key type: %s" % ktype) + return simple["value"] + + def set_secret(self, name, value): + res = self.put(name, json={"type": "simple", "value": value}) + res.raise_for_status() + + def del_secret(self, name): + res = self.delete(name) + res.raise_for_status() + + def create_container(self, name): + res = self.post(name) + res.raise_for_status() diff --git a/src/tests/intg/test_secrets.py b/src/tests/intg/test_secrets.py new file mode 100644 index 0000000000000000000000000000000000000000..e394d1275e35e686a14a604943796e793fe29119 --- /dev/null +++ b/src/tests/intg/test_secrets.py @@ -0,0 +1,162 @@ +# +# Secrets responder integration tests +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This 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; version 2 only +# +# This program 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 program. If not, see . +# + +import os +import stat +import config +import signal +import subprocess +import time +import socket +import pytest +from requests import HTTPError + +from util import unindent +from secrets import SecretsLocalClient + + +def create_conf_fixture(request, contents): + """Generate sssd.conf and add teardown for removing it""" + conf = open(config.CONF_PATH, "w") + conf.write(contents) + conf.close() + os.chmod(config.CONF_PATH, stat.S_IRUSR | stat.S_IWUSR) + request.addfinalizer(lambda: os.unlink(config.CONF_PATH)) + + +def create_sssd_secrets_fixture(request): + if subprocess.call(['sssd', "--genconf"]) != 0: + raise Exception("failed to regenerate confdb") + + resp_path = os.path.join(config.LIBEXEC_PATH, "sssd", "sssd_secrets") + + secpid = os.fork() + if secpid == 0: + if subprocess.call([resp_path, "--uid=0", "--gid=0"]) != 0: + raise Exception("sssd_secrets failed to start") + + sock_path = os.path.join(config.RUNSTATEDIR, "secrets.socket") + sck = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + for _ in range(1, 10): + try: + sck.connect(sock_path) + except: + time.sleep(0.1) + else: + break + sck.close() + + def sec_teardown(): + if secpid == 0: + return + + os.kill(secpid, signal.SIGTERM) + for secdb_file in os.listdir(config.SECDB_PATH): + os.unlink(config.SECDB_PATH + "/" + secdb_file) + request.addfinalizer(sec_teardown) + + +@pytest.fixture +def setup_for_secrets(request): + """ + Just set up the local provider for tests and enable the secrets + responder + """ + conf = unindent("""\ + [sssd] + domains = local + services = nss + + [domain/local] + id_provider = local + """).format(**locals()) + + create_conf_fixture(request, conf) + create_sssd_secrets_fixture(request) + return None + + +@pytest.fixture +def secrets_cli(request): + sock_path = os.path.join(config.RUNSTATEDIR, "secrets.socket") + cli = SecretsLocalClient(sock_path=sock_path) + return cli + + +def test_crd_ops(setup_for_secrets, secrets_cli): + """ + Test that the basic Create, Retrieve, Delete operations work + """ + cli = secrets_cli + + # Listing a totally empty database yields a 404 error, no secrets are there + with pytest.raises(HTTPError) as err404: + secrets = cli.list_secrets() + assert str(err404.value).startswith("404") + + # Set some value, should succeed + cli.set_secret("foo", "bar") + + fooval = cli.get_secret("foo") + assert fooval == "bar" + + # Listing secrets should work now as well + secrets = cli.list_secrets() + assert len(secrets) == 1 + assert "foo" in secrets + + # Overwriting a secret is an error + with pytest.raises(HTTPError) as err409: + cli.set_secret("foo", "baz") + assert str(err409.value).startswith("409") + + # Delete a secret + cli.del_secret("foo") + with pytest.raises(HTTPError) as err404: + fooval = cli.get_secret("foo") + assert str(err404.value).startswith("404") + + # Delete a non-existent secret must yield a 404 + with pytest.raises(HTTPError) as err404: + cli.del_secret("foo") + assert str(err404.value).startswith("404") + + +def test_containers(setup_for_secrets, secrets_cli): + """ + Test that storing secrets inside containers works + """ + cli = secrets_cli + + # No trailing slash, no game.. + with pytest.raises(HTTPError) as err400: + cli.create_container("mycontainer") + assert str(err400.value).startswith("400") + + cli.create_container("mycontainer/") + cli.set_secret("mycontainer/foo", "containedfooval") + assert cli.get_secret("mycontainer/foo") == "containedfooval" + + # Removing a non-empty container should not succeed + with pytest.raises(HTTPError) as err409: + cli.del_secret("mycontainer/") + assert str(err409.value).startswith("409") + + # Try removing the secret first, then the container + cli.del_secret("mycontainer/foo") + cli.del_secret("mycontainer/") -- 2.9.3