sssd/0068-TESTS-Add-integration-...

396 lines
12 KiB
Diff

From 0718b1bf4af69712d18f6ea3a427c1cab2e377da Mon Sep 17 00:00:00 2001
From: Jakub Hrozek <jhrozek@redhat.com>
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 <pcech@redhat.com>
Reviewed-by: Lukáš Slebodník <lslebodn@redhat.com>
(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 <http://www.gnu.org/licenses/>.
+#
+
+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 <http://www.gnu.org/licenses/>.
+#
+
+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