396 lines
12 KiB
Diff
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
|
|
|