From b5e7323845f60a980626fea24982b350137bc6fe Mon Sep 17 00:00:00 2001 From: Peter Hamilton Date: Tue, 8 May 2018 17:28:01 -0400 Subject: [PATCH] Add functional tests for server auth and access control This change adds a new integration test suite, named 'functional', that is specifically intended to test third-party authentication and group-based access control with the PyKMIP server. A new tox environment is added to handle running these tests separately from the existing 'integration' test suite. New Travis CI configuration and setup files have also been added to facilitate running these tests automatically. --- .travis.yml | 37 +++ .travis/functional/pykmip/certs/dummy.txt | 1 + .travis/functional/pykmip/client.conf | 51 ++++ .../functional/pykmip/policies/policy.json | 24 ++ .travis/functional/pykmip/server.conf | 19 ++ .travis/functional/slugs/slugs.conf | 12 + .../functional/slugs/user_group_mapping.csv | 4 + .travis/run.sh | 15 + kmip/tests/functional/__init__.py | 0 kmip/tests/functional/conftest.py | 29 ++ kmip/tests/functional/services/__init__.py | 0 .../services/test_authentication.py | 263 ++++++++++++++++++ requirements.txt | 2 +- test-requirements.txt | 1 + tox.ini | 7 + 15 files changed, 464 insertions(+), 1 deletion(-) create mode 100644 .travis/functional/pykmip/certs/dummy.txt create mode 100644 .travis/functional/pykmip/client.conf create mode 100644 .travis/functional/pykmip/policies/policy.json create mode 100644 .travis/functional/pykmip/server.conf create mode 100644 .travis/functional/slugs/slugs.conf create mode 100644 .travis/functional/slugs/user_group_mapping.csv create mode 100644 kmip/tests/functional/__init__.py create mode 100644 kmip/tests/functional/conftest.py create mode 100644 kmip/tests/functional/services/__init__.py create mode 100644 kmip/tests/functional/services/test_authentication.py diff --git a/.travis.yml b/.travis.yml index aff2b58..774f3d6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,14 @@ matrix: os: linux dist: trusty env: TOXENV=py27 RUN_INTEGRATION_TESTS=1 + - python: 2.7 + os: linux + dist: precise + env: TOXENV=py27 RUN_INTEGRATION_TESTS=2 + - python: 2.7 + os: linux + dist: trusty + env: TOXENV=py27 RUN_INTEGRATION_TESTS=2 - python: 3.4 os: linux dist: precise @@ -34,6 +42,14 @@ matrix: os: linux dist: trusty env: TOXENV=py34 RUN_INTEGRATION_TESTS=1 + - python: 3.4 + os: linux + dist: precise + env: TOXENV=py34 RUN_INTEGRATION_TESTS=2 + - python: 3.4 + os: linux + dist: trusty + env: TOXENV=py34 RUN_INTEGRATION_TESTS=2 - python: 3.5 os: linux dist: precise @@ -50,6 +66,14 @@ matrix: os: linux dist: trusty env: TOXENV=py35 RUN_INTEGRATION_TESTS=1 + - python: 3.5 + os: linux + dist: precise + env: TOXENV=py35 RUN_INTEGRATION_TESTS=2 + - python: 3.5 + os: linux + dist: trusty + env: TOXENV=py35 RUN_INTEGRATION_TESTS=2 - python: 3.6 os: linux dist: precise @@ -66,6 +90,14 @@ matrix: os: linux dist: trusty env: TOXENV=py36 RUN_INTEGRATION_TESTS=1 + - python: 3.6 + os: linux + dist: precise + env: TOXENV=py36 RUN_INTEGRATION_TESTS=2 + - python: 3.6 + os: linux + dist: trusty + env: TOXENV=py36 RUN_INTEGRATION_TESTS=2 - python: 2.7 os: linux dist: precise @@ -91,9 +123,14 @@ matrix: dist: trusty env: TOXENV=docs RUN_INTEGRATION_TESTS=0 install: + # Pin six to >= 1.11.0 to avoid setuptools/pip race condition + # For more info, see: https://github.com/OpenKMIP/PyKMIP/issues/435 + - pip uninstall -y six + - pip install six>=1.11.0 - pip install tox - pip install bandit - pip install codecov + - pip install slugs - python setup.py install script: - ./.travis/run.sh diff --git a/.travis/functional/pykmip/certs/dummy.txt b/.travis/functional/pykmip/certs/dummy.txt new file mode 100644 index 0000000..b79e975 --- /dev/null +++ b/.travis/functional/pykmip/certs/dummy.txt @@ -0,0 +1 @@ +Dummy file to ensure ./certs gets copied with the ./pykmip directory. diff --git a/.travis/functional/pykmip/client.conf b/.travis/functional/pykmip/client.conf new file mode 100644 index 0000000..c19a935 --- /dev/null +++ b/.travis/functional/pykmip/client.conf @@ -0,0 +1,51 @@ +[john_doe] +host=127.0.0.1 +port=5696 +certfile=/tmp/pykmip/certs/client_certificate_john_doe.pem +keyfile=/tmp/pykmip/certs/client_key_john_doe.pem +ca_certs=/tmp/pykmip/certs/root_certificate.pem +cert_reqs=CERT_REQUIRED +ssl_version=PROTOCOL_SSLv23 +do_handshake_on_connect=True +suppress_ragged_eofs=True +username=John Doe +password=secret1 + +[jane_doe] +host=127.0.0.1 +port=5696 +certfile=/tmp/pykmip/certs/client_certificate_jane_doe.pem +keyfile=/tmp/pykmip/certs/client_key_jane_doe.pem +ca_certs=/tmp/pykmip/certs/root_certificate.pem +cert_reqs=CERT_REQUIRED +ssl_version=PROTOCOL_SSLv23 +do_handshake_on_connect=True +suppress_ragged_eofs=True +username=Jane Doe +password=secret2 + +[john_smith] +host=127.0.0.1 +port=5696 +certfile=/tmp/pykmip/certs/client_certificate_john_smith.pem +keyfile=/tmp/pykmip/certs/client_key_john_smith.pem +ca_certs=/tmp/pykmip/certs/root_certificate.pem +cert_reqs=CERT_REQUIRED +ssl_version=PROTOCOL_SSLv23 +do_handshake_on_connect=True +suppress_ragged_eofs=True +username=John Smith +password=secret3 + +[jane_smith] +host=127.0.0.1 +port=5696 +certfile=/tmp/pykmip/certs/client_certificate_jane_smith.pem +keyfile=/tmp/pykmip/certs/client_key_jane_smith.pem +ca_certs=/tmp/pykmip/certs/root_certificate.pem +cert_reqs=CERT_REQUIRED +ssl_version=PROTOCOL_SSLv23 +do_handshake_on_connect=True +suppress_ragged_eofs=True +username=Jane Smith +password=secret4 diff --git a/.travis/functional/pykmip/policies/policy.json b/.travis/functional/pykmip/policies/policy.json new file mode 100644 index 0000000..a072e5c --- /dev/null +++ b/.travis/functional/pykmip/policies/policy.json @@ -0,0 +1,24 @@ +{ + "policy_1": { + "groups": { + "Group A": { + "SYMMETRIC_KEY": { + "GET": "ALLOW_ALL", + "DESTROY": "ALLOW_ALL" + } + }, + "Group B": { + "SYMMETRIC_KEY": { + "GET": "ALLOW_ALL", + "DESTROY": "DISALLOW_ALL" + } + } + }, + "default": { + "SYMMETRIC_KEY": { + "GET": "DISALLOW_ALL", + "DESTROY": "DISALLOW_ALL" + } + } + } +} \ No newline at end of file diff --git a/.travis/functional/pykmip/server.conf b/.travis/functional/pykmip/server.conf new file mode 100644 index 0000000..5f8dc9e --- /dev/null +++ b/.travis/functional/pykmip/server.conf @@ -0,0 +1,19 @@ +[server] +hostname=127.0.0.1 +port=5696 +certificate_path=/tmp/pykmip/certs/server_certificate.pem +key_path=/tmp/pykmip/certs/server_key.pem +ca_path=/tmp/pykmip/certs/root_certificate.pem +auth_suite=Basic +policy_path=/tmp/pykmip/policies +enable_tls_client_auth=True +tls_cipher_suites= + TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256 + AES128-SHA256 + TLS_RSA_WITH_AES_256_CBC_SHA256 + AES256-SHA256 +logging_level=DEBUG + +[auth:slugs] +enabled=True +url=http://127.0.0.1:8080/slugs/ diff --git a/.travis/functional/slugs/slugs.conf b/.travis/functional/slugs/slugs.conf new file mode 100644 index 0000000..9c01e3d --- /dev/null +++ b/.travis/functional/slugs/slugs.conf @@ -0,0 +1,12 @@ +[global] +environment = 'production' +server.socket_host = '127.0.0.1' +server.socket_port = 8080 +log.access_file = '/tmp/slugs/access.log' +log.error_file = '/tmp/slugs/error.log' + +[data] +user_group_mapping = '/tmp/slugs/user_group_mapping.csv' + +[/slugs] +tools.trailing_slash.on = True diff --git a/.travis/functional/slugs/user_group_mapping.csv b/.travis/functional/slugs/user_group_mapping.csv new file mode 100644 index 0000000..bfbe552 --- /dev/null +++ b/.travis/functional/slugs/user_group_mapping.csv @@ -0,0 +1,4 @@ +John Doe,Group A +Jane Doe,Group A +Jane Doe,Group B +John Smith,Group B diff --git a/.travis/run.sh b/.travis/run.sh index 50277bf..7baf0fb 100755 --- a/.travis/run.sh +++ b/.travis/run.sh @@ -16,6 +16,21 @@ if [[ "${RUN_INTEGRATION_TESTS}" == "1" ]]; then sudo chmod 777 /var/log/pykmip python ./bin/run_server.py & tox -e integration -- --config client +elif [[ "${RUN_INTEGRATION_TESTS}" == "2" ]]; then + # Set up the SLUGS instance + cp -r ./.travis/functional/slugs /tmp/ + slugs -c /tmp/slugs/slugs.conf & + + # Set up the PyKMIP server + cp -r ./.travis/functional/pykmip /tmp/ + python ./bin/create_certificates.py + mv *.pem /tmp/pykmip/certs/ + sudo mkdir /var/log/pykmip + sudo chmod 777 /var/log/pykmip + pykmip-server -f /tmp/pykmip/server.conf -l /tmp/pykmip/server.log & + + # Run the functional tests + tox -e functional -- --config-file /tmp/pykmip/client.conf else tox fi diff --git a/kmip/tests/functional/__init__.py b/kmip/tests/functional/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kmip/tests/functional/conftest.py b/kmip/tests/functional/conftest.py new file mode 100644 index 0000000..876a5b8 --- /dev/null +++ b/kmip/tests/functional/conftest.py @@ -0,0 +1,29 @@ +# Copyright (c) 2018 The Johns Hopkins University/Applied Physics Laboratory +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import pytest + + +def pytest_addoption(parser): + parser.addoption( + "--config-file", + action="store", + help="Config file path for client configuration settings" + ) + + +@pytest.fixture(scope="class") +def config_file(request): + request.cls.config_file = request.config.getoption("--config-file") diff --git a/kmip/tests/functional/services/__init__.py b/kmip/tests/functional/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kmip/tests/functional/services/test_authentication.py b/kmip/tests/functional/services/test_authentication.py new file mode 100644 index 0000000..6f912ea --- /dev/null +++ b/kmip/tests/functional/services/test_authentication.py @@ -0,0 +1,263 @@ +# Copyright (c) 2018 The Johns Hopkins University/Applied Physics Laboratory +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os +import pytest +import six +import testtools +import time + +from kmip.core import enums +from kmip.pie import client +from kmip.pie import exceptions +from kmip.pie import objects + + +@pytest.mark.usefixtures("config_file") +class TestSLUGSAuthenticationAndAccessControl(testtools.TestCase): + + def setUp(self): + super(TestSLUGSAuthenticationAndAccessControl, self).setUp() + + self.client_john_doe = client.ProxyKmipClient( + config='john_doe', + config_file=self.config_file + ) + self.client_jane_doe = client.ProxyKmipClient( + config='jane_doe', + config_file=self.config_file + ) + self.client_john_smith = client.ProxyKmipClient( + config='john_smith', + config_file=self.config_file + ) + self.client_jane_smith = client.ProxyKmipClient( + config='jane_smith', + config_file=self.config_file + ) + + def tearDown(self): + super(TestSLUGSAuthenticationAndAccessControl, self).tearDown() + + def test_group_level_access_control(self): + """ + Test that: + 1. a user in Group A can create and retrieve a symmetric key + 2. a user in Group B can also retrieve the same symmetric key + 3. a user in both Groups can also retrieve the same symmetric key + 4. a user in Group B cannot destroy the same symmetric key, and + 5. a user in Group A can destroy the same symmetric key. + """ + with self.client_john_doe as c: + uid = c.create( + enums.CryptographicAlgorithm.AES, + 256, + operation_policy_name="policy_1" + ) + self.assertIsInstance(uid, six.string_types) + + key = c.get(uid) + self.assertIsInstance(key, objects.SymmetricKey) + self.assertEqual( + key.cryptographic_algorithm, + enums.CryptographicAlgorithm.AES) + self.assertEqual(key.cryptographic_length, 256) + + with self.client_jane_doe as c: + key = c.get(uid) + self.assertIsInstance(key, objects.SymmetricKey) + self.assertEqual( + key.cryptographic_algorithm, + enums.CryptographicAlgorithm.AES) + self.assertEqual(key.cryptographic_length, 256) + + with self.client_john_smith as c: + key = c.get(uid) + self.assertIsInstance(key, objects.SymmetricKey) + self.assertEqual( + key.cryptographic_algorithm, + enums.CryptographicAlgorithm.AES) + self.assertEqual(key.cryptographic_length, 256) + + self.assertRaises(exceptions.KmipOperationFailure, c.destroy, uid) + + with self.client_john_doe as c: + c.destroy(uid) + self.assertRaises( + exceptions.KmipOperationFailure, c.get, uid) + self.assertRaises( + exceptions.KmipOperationFailure, c.destroy, uid) + + def test_policy_live_loading(self): + """ + Test that: + 1. a user in Group A can create and retrieve a symmetric key + 2. a user in Group B can also retrieve the same symmetric key + 3. a user in Group B cannot destroy the same symmetric key + 4. a policy is uploaded if created after server start up + 5. a user in Group A cannot retrieve the same symmetric key, and + 6. a user in Group B can destroy the same symmetric key. + """ + with self.client_john_doe as c: + uid = c.create( + enums.CryptographicAlgorithm.AES, + 256, + operation_policy_name="policy_1" + ) + self.assertIsInstance(uid, six.string_types) + + key = c.get(uid) + self.assertIsInstance(key, objects.SymmetricKey) + self.assertEqual( + key.cryptographic_algorithm, + enums.CryptographicAlgorithm.AES) + self.assertEqual(key.cryptographic_length, 256) + + with self.client_john_smith as c: + key = c.get(uid) + self.assertIsInstance(key, objects.SymmetricKey) + self.assertEqual( + key.cryptographic_algorithm, + enums.CryptographicAlgorithm.AES) + self.assertEqual(key.cryptographic_length, 256) + + self.assertRaises(exceptions.KmipOperationFailure, c.destroy, uid) + + with open("/tmp/pykmip/policies/policy_overwrite.json", "w") as f: + f.write('{\n') + f.write(' "policy_1": {\n') + f.write(' "groups": {\n') + f.write(' "Group A": {\n') + f.write(' "SYMMETRIC_KEY": {\n') + f.write(' "GET": "DISALLOW_ALL",\n') + f.write(' "DESTROY": "DISALLOW_ALL"\n') + f.write(' }\n') + f.write(' },\n') + f.write(' "Group B": {\n') + f.write(' "SYMMETRIC_KEY": {\n') + f.write(' "GET": "ALLOW_ALL",\n') + f.write(' "DESTROY": "ALLOW_ALL"\n') + f.write(' }\n') + f.write(' }\n') + f.write(' }\n') + f.write(' }\n') + f.write('}\n') + time.sleep(1) + + with self.client_john_doe as c: + self.assertRaises(exceptions.KmipOperationFailure, c.get, uid) + self.assertRaises(exceptions.KmipOperationFailure, c.destroy, uid) + + with self.client_john_smith as c: + key = c.get(uid) + self.assertIsInstance(key, objects.SymmetricKey) + self.assertEqual( + key.cryptographic_algorithm, + enums.CryptographicAlgorithm.AES) + self.assertEqual(key.cryptographic_length, 256) + + c.destroy(uid) + self.assertRaises( + exceptions.KmipOperationFailure, c.get, uid) + self.assertRaises( + exceptions.KmipOperationFailure, c.destroy, uid) + + os.remove("/tmp/pykmip/policies/policy_overwrite.json") + time.sleep(1) + + def test_policy_caching(self): + """ + Test that: + 1. a user in Group A can create and retrieve a symmetric key + 2. a policy is uploaded if created after server start up + 3. a user in Group A cannot retrieve or destroy the same symmetric key + 4. the original policy is restored after the new policy is removed, and + 5. a user in Group A can retrieve and destroy the same symmetric key. + """ + with self.client_john_doe as c: + uid = c.create( + enums.CryptographicAlgorithm.AES, + 256, + operation_policy_name="policy_1" + ) + self.assertIsInstance(uid, six.string_types) + + key = c.get(uid) + self.assertIsInstance(key, objects.SymmetricKey) + self.assertEqual( + key.cryptographic_algorithm, + enums.CryptographicAlgorithm.AES) + self.assertEqual(key.cryptographic_length, 256) + + with open("/tmp/pykmip/policies/policy_caching.json", "w") as f: + f.write('{\n') + f.write(' "policy_1": {\n') + f.write(' "groups": {\n') + f.write(' "Group A": {\n') + f.write(' "SYMMETRIC_KEY": {\n') + f.write(' "GET": "DISALLOW_ALL",\n') + f.write(' "DESTROY": "DISALLOW_ALL"\n') + f.write(' }\n') + f.write(' }\n') + f.write(' }\n') + f.write(' }\n') + f.write('}\n') + time.sleep(1) + + self.assertRaises(exceptions.KmipOperationFailure, c.get, uid) + self.assertRaises(exceptions.KmipOperationFailure, c.destroy, uid) + + os.remove("/tmp/pykmip/policies/policy_caching.json") + time.sleep(1) + + key = c.get(uid) + self.assertIsInstance(key, objects.SymmetricKey) + self.assertEqual( + key.cryptographic_algorithm, + enums.CryptographicAlgorithm.AES) + self.assertEqual(key.cryptographic_length, 256) + + c.destroy(uid) + self.assertRaises( + exceptions.KmipOperationFailure, c.get, uid) + self.assertRaises( + exceptions.KmipOperationFailure, c.destroy, uid) + + def test_authenticating_unrecognized_user(self): + """ + Test that an unrecognized user is blocked from submitting a request. + """ + with open("/tmp/slugs/user_group_mapping.csv", "w") as f: + f.write('Jane Doe,Group A\n') + f.write('Jane Doe,Group B\n') + f.write('John Smith,Group B\n') + time.sleep(1) + + args = (enums.CryptographicAlgorithm.AES, 256) + kwargs = {'operation_policy_name': 'policy_1'} + with self.client_john_doe as c: + self.assertRaises( + exceptions.KmipOperationFailure, + c.create, + *args, + **kwargs + ) + + with open("/tmp/slugs/user_group_mapping.csv", "w") as f: + f.write('John Doe,Group A\n') + f.write('Jane Doe,Group A\n') + f.write('Jane Doe,Group B\n') + f.write('John Smith,Group B\n') + time.sleep(1) diff --git a/requirements.txt b/requirements.txt index 414594c..f1856aa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ cryptography>=1.3 enum34 requests -six>=1.9.0 +six>=1.11.0 sqlalchemy>=1.0 diff --git a/test-requirements.txt b/test-requirements.txt index 5cc044c..50cdb4a 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -5,6 +5,7 @@ testtools fixtures testresources mock +slugs testscenarios testrepository sphinx diff --git a/tox.ini b/tox.ini index 920cb6b..cdbb082 100644 --- a/tox.ini +++ b/tox.ini @@ -22,6 +22,13 @@ basepython=python2.7 commands = py.test --strict kmip/tests/integration -m "not ignore" {posargs} +[testenv:functional] +# Note: This requires local access to instances of the PyKMIP server and SLUGS. +deps = {[testenv]deps} +basepython=python2.7 +commands = + py.test --strict kmip/tests/functional -m "not ignore" {posargs} + [testenv:bandit] deps = {[testenv]deps} commands = bandit -r kmip -n5 -x kmip/tests