diff options
author | Jakub Hrozek <jhrozek@redhat.com> | 2016-08-08 17:49:05 +0200 |
---|---|---|
committer | Lukas Slebodnik <lslebodn@redhat.com> | 2016-09-20 17:34:39 +0200 |
commit | db0982c52294ee5ea08ed242d27660783fde29cd (patch) | |
tree | 1bfcbdb9263bcb6dc209f47d1ce0e25d0e484d3e /src/tests/intg | |
parent | 4f2509f8d23d9e921f07b2ead63392ae82ad3a38 (diff) | |
download | sssd-db0982c52294ee5ea08ed242d27660783fde29cd.tar.gz sssd-db0982c52294ee5ea08ed242d27660783fde29cd.tar.xz sssd-db0982c52294ee5ea08ed242d27660783fde29cd.zip |
TESTS: Add integration tests for the sssd-secrets
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>
Diffstat (limited to 'src/tests/intg')
-rw-r--r-- | src/tests/intg/Makefile.am | 5 | ||||
-rw-r--r-- | src/tests/intg/config.py.m4 | 3 | ||||
-rw-r--r-- | src/tests/intg/secrets.py | 137 | ||||
-rw-r--r-- | src/tests/intg/test_secrets.py | 162 |
4 files changed, 307 insertions, 0 deletions
diff --git a/src/tests/intg/Makefile.am b/src/tests/intg/Makefile.am index 75422a441..1e08eadcb 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 77aa47b79..65e17e55a 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 000000000..5d4c0e2f2 --- /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 000000000..e394d1275 --- /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/") |