diff --git a/src/fts3rest/fts3rest/controllers/delegation.py b/src/fts3rest/fts3rest/controllers/delegation.py index acd65d3c2591e45b5806dde161c295f5ff98edad..16ef9f2456552daa5f660f3d59b8ef643a56f148 100644 --- a/src/fts3rest/fts3rest/controllers/delegation.py +++ b/src/fts3rest/fts3rest/controllers/delegation.py @@ -112,7 +112,7 @@ def _validate_proxy(proxy_pem, private_key_pem): x509_proxy_issuer = x509_list[1] expiration_time = x509_proxy.get_not_after().get_datetime().replace(tzinfo=None) - private_key = EVP.load_key_string(str(private_key_pem), callback=_mute_callback) + private_key = EVP.load_key_string(private_key_pem, callback=_mute_callback) # The modulus of the stored private key and the modulus of the proxy must match if x509_proxy.get_pubkey().get_modulus() != private_key.get_modulus(): @@ -153,7 +153,7 @@ def _build_full_proxy(x509_pem, privkey_pem): A full proxy """ x509_list = _read_x509_list(x509_pem) - x509_chain = "".join(map(lambda x: x.as_pem(), x509_list[1:])) + x509_chain = b"".join(map(lambda x: x.as_pem(), x509_list[1:])) return x509_list[0].as_pem() + privkey_pem + x509_chain @@ -331,11 +331,13 @@ class credential(Delegation): log.debug("Received delegated credentials for %s" % dlg_id) log.debug(x509_proxy_pem) + if credential_cache.priv_key and isinstance(credential_cache.priv_key, str): + priv_key = bytes(credential_cache.priv_key, "utf-8") + else: + priv_key = credential_cache.priv_key try: - expiration_time = _validate_proxy(x509_proxy_pem, credential_cache.priv_key) - x509_full_proxy_pem = _build_full_proxy( - x509_proxy_pem, credential_cache.priv_key - ) + expiration_time = _validate_proxy(x509_proxy_pem, priv_key) + x509_full_proxy_pem = _build_full_proxy(x509_proxy_pem, priv_key) except ProxyException as ex: raise BadRequest("Could not process the proxy: " + str(ex)) diff --git a/src/fts3rest/fts3rest/tests/__init__.py b/src/fts3rest/fts3rest/tests/__init__.py index c031d6afc219a0d1ea20ea85c41ffa5ec245e22f..ee046677309bce3f771a192723285329aeec28d8 100644 --- a/src/fts3rest/fts3rest/tests/__init__.py +++ b/src/fts3rest/fts3rest/tests/__init__.py @@ -129,7 +129,7 @@ class TestController(TestCase): if subject is None: subject = issuer + [("CN", "proxy")] - x509_request = X509.load_request_string(str(request_pem)) + x509_request = X509.load_request_string(request_pem) not_before = ASN1.ASN1_UTCTIME() not_before.set_datetime(datetime.now(UTC)) diff --git a/src/fts3rest/fts3rest/tests/functional/test_delegation.py b/src/fts3rest/fts3rest/tests/functional/test_delegation.py new file mode 100644 index 0000000000000000000000000000000000000000..8dae6d96002e7072b5d4b33bac11954f1093fff2 --- /dev/null +++ b/src/fts3rest/fts3rest/tests/functional/test_delegation.py @@ -0,0 +1,277 @@ +from datetime import datetime, timedelta +from M2Crypto import EVP +import time + +from fts3rest.controllers.delegation import _generate_proxy_request +from fts3rest.tests import TestController +from fts3rest.model.meta import Session +from fts3.model import Credential, CredentialCache + + +class TestDelegation(TestController): + """ + Tests for the delegation controller + """ + + def test_get_termination_time_not_existing(self): + """ + Get the termination time for a dlg_id that hasn't delegated yet + """ + self.setup_gridsite_environment() + creds = self.get_user_credentials() + + delegation_id = self.app.get( + url="/delegation/%s" % creds.delegation_id, status=200 + ).json + self.assertEqual( + delegation_id, [] + ) # for Flask, I changed None to [], as it makes more sense + + def test_get_termination_time(self): + """ + Get credentials termination time + """ + self.test_valid_proxy() + creds = self.get_user_credentials() + + termination_str = self.app.get( + url="/delegation/%s" % creds.delegation_id, status=200 + ).json["termination_time"] + + termination = datetime.strptime(termination_str, "%Y-%m-%dT%H:%M:%S") + self.assertGreater( + termination, datetime.utcnow() + timedelta(hours=2, minutes=58) + ) + + def test_put_cred_without_cache(self): + """ + This is a regression test. It tries to PUT directly + credentials without the previous negotiation, so there is no + CredentialCache in the database. This attempt must fail. + """ + self.setup_gridsite_environment() + creds = self.get_user_credentials() + + request = self.app.get( + url="/delegation/%s/request" % creds.delegation_id, status=200 + ) + proxy = self.get_x509_proxy(request.get_data(as_text=True)) + + Session.delete( + Session.query(CredentialCache).get((creds.delegation_id, creds.user_dn)) + ) + + self.app.put( + url="/delegation/%s/credential" % creds.delegation_id, + params=proxy, + status=400, + ) + + def test_put_malformed_pem(self): + """ + Putting a malformed proxy must fail + """ + self.setup_gridsite_environment() + creds = self.get_user_credentials() + + self.app.get(url="/delegation/%s/request" % creds.delegation_id, status=200) + + self.app.put( + url="/delegation/%s/credential" % creds.delegation_id, + params="MALFORMED!!!1", + status=400, + ) + + def test_valid_proxy(self): + """ + Putting a well-formed proxy with all the right steps must succeed + """ + self.setup_gridsite_environment() + creds = self.get_user_credentials() + + request = self.app.get( + url="/delegation/%s/request" % creds.delegation_id, status=200 + ) + proxy = self.get_x509_proxy(request.get_data(as_text=True)) + + self.app.put( + url="/delegation/%s/credential" % creds.delegation_id, + params=proxy, + status=201, + ) + + proxy = Session.query(Credential).get((creds.delegation_id, creds.user_dn)) + self.assertNotEqual(None, proxy) + return proxy + + def test_dn_mismatch(self): + """ + A well-formed proxy with mismatching issuer and subject must fail + """ + self.setup_gridsite_environment() + creds = self.get_user_credentials() + + request = self.app.get( + url="/delegation/%s/request" % creds.delegation_id, status=200 + ) + + proxy = self.get_x509_proxy( + request.get_data(as_text=True), subject=[("DC", "dummy")] + ) + + self.app.put( + url="/delegation/%s/credential" % creds.delegation_id, + params=proxy, + status=400, + ) + + def test_signed_wrong_priv_key(self): + """ + Regression for FTS-30 + If a proxy is signed with an invalid private key, reject it + """ + self.setup_gridsite_environment() + creds = self.get_user_credentials() + + request = self.app.get( + url="/delegation/%s/request" % creds.delegation_id, status=200 + ) + + proxy = self.get_x509_proxy( + request.get_data(as_text=True), private_key=EVP.PKey() + ) + + self.app.put( + url="/delegation/%s/credential" % creds.delegation_id, + params=proxy, + status=400, + ) + + def test_wrong_request(self): + """ + Get a request, sign a different request and send it + """ + self.setup_gridsite_environment() + creds = self.get_user_credentials() + + self.app.get(url="/delegation/%s/request" % creds.delegation_id, status=200) + + (different_request, _) = _generate_proxy_request() + proxy = self.get_x509_proxy(different_request.as_pem(), private_key=EVP.PKey()) + + self.app.put( + url="/delegation/%s/credential" % creds.delegation_id, + params=proxy, + status=400, + ) + + def test_get_request_different_dlg_id(self): + """ + A user should be able only to get his/her own proxy request, + and be denied any other. + """ + self.setup_gridsite_environment() + + self.app.get(url="/delegation/12345xx/request", status=403) + + def test_view_different_dlg_id(self): + """ + A user should be able only to get his/her own delegation information. + """ + self.setup_gridsite_environment() + + self.app.get(url="/delegation/12345x", status=403) + + def test_remove_delegation(self): + """ + A user should be able to remove his/her proxy + """ + self.setup_gridsite_environment() + creds = self.get_user_credentials() + + self.test_valid_proxy() + + self.app.delete(url="/delegation/%s" % creds.delegation_id, status=204) + + self.app.delete(url="/delegation/%s" % creds.delegation_id, status=404) + + proxy = Session.query(Credential).get((creds.delegation_id, creds.user_dn)) + + self.assertEqual(None, proxy) + + def test_set_voms(self): + """ + The server must regenerate a proxy with VOMS extensions + Need a real proxy for this one + """ + self.setup_gridsite_environment() + creds = self.get_user_credentials() + + # Need to push a real proxy :/ + proxy_pem = self.get_real_x509_proxy() + if proxy_pem is None: + self.skipTest("Could not get a valid real proxy for test_set_voms") + + proxy = Credential() + proxy.dn = creds.user_dn + proxy.dlg_id = creds.delegation_id + proxy.termination_time = datetime.utcnow() + timedelta(hours=1) + proxy.proxy = proxy_pem + Session.merge(proxy) + Session.commit() + + # Now, request the voms extensions + self.app.post_json( + url="/delegation/%s/voms" % creds.delegation_id, + params=["dteam:/dteam/Role=lcgadmin"], + status=203, + ) + + # And validate + proxy2 = Session.query(Credential).get((creds.delegation_id, creds.user_dn)) + self.assertNotEqual(proxy.proxy, proxy2.proxy) + self.assertEqual("dteam:/dteam/Role=lcgadmin", proxy2.voms_attrs) + + def test_delegate_rfc(self): + """ + Delegate an RFC-like proxy + """ + self.setup_gridsite_environment() + creds = self.get_user_credentials() + + request = self.app.get( + url="/delegation/%s/request" % creds.delegation_id, status=200 + ) + + proxy = self.get_x509_proxy( + request.get_data(as_text=True), + subject=[ + ("DC", "ch"), + ("DC", "cern"), + ("CN", "Test User"), + ("CN", str(int(time.time()))), + ], + ) + + self.app.put( + url="/delegation/%s/credential" % creds.delegation_id, + params=proxy, + status=201, + ) + + proxy = Session.query(Credential).get((creds.delegation_id, creds.user_dn)) + self.assertNotEqual(None, proxy) + + def test_cert(self, cert=None): + """ + Test for returning the user certificate + """ + self.setup_gridsite_environment() + if cert is None: + cert = "SSL_CLIENT_CERT" + self.app.environ_base["SSL_CLIENT_CERT"] = "certificate:" + cert + + returns = self.app.get(url="/whoami/certificate", status=200).get_data( + as_text=True + ) + self.assertEqual("certificate:" + cert, returns)