diff --git a/doc/source/sdk/proxies/index.rst b/doc/source/sdk/proxies/index.rst index 54c01ca98..4c8d09a9b 100644 --- a/doc/source/sdk/proxies/index.rst +++ b/doc/source/sdk/proxies/index.rst @@ -20,6 +20,7 @@ Service Proxies Relational Database Service RDS V3 (RDS) Volume Backup Service (VBS) Virtual Private Cloud (VPC) + Web Application Firewall (WAF) .. _service-proxies: diff --git a/doc/source/sdk/proxies/waf.rst b/doc/source/sdk/proxies/waf.rst new file mode 100644 index 000000000..ea6a880a2 --- /dev/null +++ b/doc/source/sdk/proxies/waf.rst @@ -0,0 +1,22 @@ +Web Application Firewall API +============================ + +.. automodule:: otcextensions.sdk.waf.v1._proxy + +The WAF Service Class +--------------------- + +The waf high-level interface is available through the ``waf`` +member of a :class:`~openstack.connection.Connection` object. The +``waf`` member will only be added if the +``otcextensions.sdk.register_otc_extensions(conn)`` method is called. + +Certificate Operations +^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: otcextensions.sdk.waf.v1._proxy.Proxy + :noindex: + :members: certificates, create_certificate, get_certificate, + delete_certificate, update_certificate, find_certificate + + diff --git a/otcextensions/sdk/__init__.py b/otcextensions/sdk/__init__.py index 1c1c403b2..3394b7fd8 100644 --- a/otcextensions/sdk/__init__.py +++ b/otcextensions/sdk/__init__.py @@ -149,7 +149,7 @@ }, 'waf': { 'service_type': 'waf', - 'append_project_id': True, + 'set_endpoint_override': True } } diff --git a/otcextensions/sdk/waf/v1/_base.py b/otcextensions/sdk/waf/v1/_base.py new file mode 100644 index 000000000..1cb544b8c --- /dev/null +++ b/otcextensions/sdk/waf/v1/_base.py @@ -0,0 +1,68 @@ +# 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. +from openstack import exceptions +from openstack import resource + + +class Resource(resource.Resource): + + @classmethod + def find(cls, session, name_or_id, ignore_missing=True, **params): + """Find a resource by its name or id. + + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param name_or_id: This resource's identifier, if needed by + the request. The default is ``None``. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :param dict params: Any additional parameters to be passed into + underlying methods, such as to + :meth:`~openstack.resource.Resource.existing` + in order to pass on URI parameters. + + :return: The :class:`Resource` object matching the given name or id + or None if nothing matches. + :raises: :class:`openstack.exceptions.DuplicateResource` if more + than one resource is found for this request. + :raises: :class:`openstack.exceptions.ResourceNotFound` if nothing + is found and ignore_missing is ``False``. + """ + session = cls._get_session(session) + # Try to short-circuit by looking directly for a matching ID. + try: + match = cls.existing( + id=name_or_id, + connection=session._get_connection(), + **params) + return match.fetch(session, **params) + except exceptions.SDKException: + # WAF will return 400 when we try to do GET with name + pass + + if ('name' in cls._query_mapping._mapping.keys() + and 'name' not in params): + params['name'] = name_or_id + + data = cls.list(session, **params) + + result = cls._get_one_match(name_or_id, data) + if result is not None: + return result + + if ignore_missing: + return None + raise exceptions.ResourceNotFound( + "No %s found for %s" % (cls.__name__, name_or_id)) diff --git a/otcextensions/sdk/waf/v1/_proxy.py b/otcextensions/sdk/waf/v1/_proxy.py index 38cfb42e8..a8c67a9fd 100644 --- a/otcextensions/sdk/waf/v1/_proxy.py +++ b/otcextensions/sdk/waf/v1/_proxy.py @@ -11,7 +11,105 @@ # under the License. from openstack import proxy +from otcextensions.sdk.waf.v1 import certificate as _cert + class Proxy(proxy.Proxy): skip_discovery = True + + def __init__(self, session, *args, **kwargs): + super(Proxy, self).__init__(session=session, *args, **kwargs) + self.endpoint_override = \ + '%s/%s/%s' % ( + self.get_endpoint(), + 'v1', + '%(project_id)s') + self.additional_headers = { + 'x-request-source-type': 'ApiCall', + 'content-type': 'application/json' + } + + # ======== Certificates ======== + def certificates(self, **query): + """Retrieve a generator of certificates + + :param dict query: Optional query parameters to be sent to limit the + resources being returned. + * `limit`: pagination limit + * `offset`: pagination offset + + :returns: A generator of certificate + :class:`~otcextensions.sdk.waf.v1.certificate.Certificate` + instances + """ + return self._list(_cert.Certificate, **query) + + def create_certificate(self, **attrs): + """Upload certificate from attributes + + :param dict attrs: Keyword arguments which will be used to create + a :class:`~otcextensions.sdk.waf.v1.certificate.Certificate`, + comprised of the properties on the Certificate class. + :returns: The results of certificate creation + :rtype: :class:`~otcextensions.sdk.waf.v1.certificate.Certificate` + """ + print(attrs) + return self._create(_cert.Certificate, prepend_key=False, **attrs) + + def get_certificate(self, certificate): + """Get a certificate + + :param certificate: The value can be the ID of a certificate + or a :class:`~otcextensions.sdk.waf.v1.certificate.Certificate` + instance. + :returns: Certificate instance + :rtype: :class:`~otcextensions.sdk.waf.v1.certificate.Certificate` + """ + return self._get(_cert.Certificate, certificate) + + def delete_certificate(self, certificate, ignore_missing=True): + """Delete a certificate + + :param certificate: The value can be the ID of a certificate + or a :class:`~otcextensions.sdk.waf.v1.certificate.Certificate` + instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the certificate does not exist. + When set to ``True``, no exception will be set when attempting to + delete a nonexistent certificate. + + :returns: Certificate been deleted + :rtype: :class:`~otcextensions.sdk.waf.v1.certificate.Certificate` + """ + return self._delete(_cert.Certificate, certificate, + ignore_missing=ignore_missing) + + def update_certificate(self, certificate, **attrs): + """Update certificate attributes + + :param certificate: The id or an instance of + :class:`~otcextensions.sdk.waf.v1.certificate.Certificate` + :param dict attrs: attributes for update on + :class:`~otcextensions.sdk.waf.v1.certificate.Certificate` + + :rtype: :class:`~otcextensions.sdk.waf.v1.certificate.Certificate` + """ + return self._update(_cert.Certificate, certificate, **attrs) + + def find_certificate(self, name_or_id, ignore_missing=True, **attrs): + """Find a single certificate + + :param name_or_id: The name or ID of a certificate + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the certificate does not exist. + When set to ``True``, no exception will be set when attempting + to delete a nonexistent certificate. + + :returns: ``None`` + """ + return self._find(_cert.Certificate, name_or_id, + ignore_missing=ignore_missing, + **attrs) diff --git a/otcextensions/sdk/waf/v1/certificate.py b/otcextensions/sdk/waf/v1/certificate.py new file mode 100644 index 000000000..6ab55aa0b --- /dev/null +++ b/otcextensions/sdk/waf/v1/certificate.py @@ -0,0 +1,40 @@ +# 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. +from openstack import resource + +from otcextensions.sdk.waf.v1 import _base + + +class Certificate(_base.Resource): + """WAF Certificate Resource""" + resources_key = 'items' + base_path = '/waf/certificate' + + # capabilities + allow_create = True + allow_list = True + allow_fetch = True + allow_delete = True + allow_commit = True + + _query_mapping = resource.QueryParameters( + 'limit', 'offset') + + #: Properties + #: Specifies the certificate content. + content = resource.Body('content') + #: Certificate expiration time + expire_time = resource.Body('expireTime') + #: Specifies the private key content. + key = resource.Body('key') + #: Certificat uploading timestamp + timestamp = resource.Body('timestamp') diff --git a/otcextensions/tests/functional/sdk/waf/__init__.py b/otcextensions/tests/functional/sdk/waf/__init__.py index e69de29bb..503499ab1 100644 --- a/otcextensions/tests/functional/sdk/waf/__init__.py +++ b/otcextensions/tests/functional/sdk/waf/__init__.py @@ -0,0 +1,21 @@ +# 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 openstack +from otcextensions.tests.functional import base + + +class TestWaf(base.BaseFunctionalTest): + + def setUp(self): + super(TestWaf, self).setUp() + openstack.enable_logging(debug=True) + self.client = self.conn.waf diff --git a/otcextensions/tests/functional/sdk/waf/v1/test_certificate.py b/otcextensions/tests/functional/sdk/waf/v1/test_certificate.py new file mode 100644 index 000000000..5a77764e7 --- /dev/null +++ b/otcextensions/tests/functional/sdk/waf/v1/test_certificate.py @@ -0,0 +1,114 @@ +# 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 uuid + +# from openstack import resource + +from otcextensions.tests.functional.sdk.waf import TestWaf + + +class TestCertificate(TestWaf): + + _PRIVATE_KEY = """-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDrvw+CfkRMtN6I +KQK+YNKhjdWqUCnTI7YqZLDhZkoIqcvK1F2mjkcoGXOAjCjvGXf/xX35j0dGLgHK +e3AwNaQDPRWec6DuTqh9kBq9Qy7rUs6Na85wwSN8FG7z9XRuWR9NhEg24nrATUr/ +k5biBtHKiP22xI9nVws+IEoYVGOOjJ2CPt9XszaS/pdN6bQLchPSLOM5WN6BHCVn +u19RnSKFqCr8AmYx2Aqo30uFTHy1EhvSX8CnRTHNvWl7qciISqiNenXjIZyCe80n +7VB+LzSbm3HeqMDsM0euq0P5uPty2A6Uuo/TlPWlls6ZhFTP+AQ9H78kPWY9nKjl +1Ja4K0sbAgMBAAECggEANJ9oceOHkWvKRLCK2T45pjBH4oWUYHoXPq1NQnMX0Yk9 +YWA4K2aVAaF0w9wFgyG3RJOsBBn0efjpE26sY0aF/ucSvVToNmm+eJDDNz4Y6hSI +4M6QvWCPcDILdk9zFvKz5xTBHec+KVDXjec/BeMpz0D3CWYk8JdgfhStFXM46eeR +z1KBOq51x+I0VD3Ar4T3hfKG2IViwevC/7kghBw+D1U/c4stHFCXv4JlrhFET2I6 +kquGtV38fMUdWBLRVr0wBB4orm+9rpSlTvbnDuuEJcb8rKvrLkGraUhSTqepQD6M +lTN4BxY+3NqdnP/SKVBRoXr+gQsLdgPUAhkvTB8f0QKBgQD32mpyweaMZYTqZ8xF +xOBzjCTGVHNlXMMt8rz9+kJ4krJ77R3L07qf+mo5bsOB2ZybHhTy7+G6QO8TXyrI +60nbpoFR0eyWy6kdn4NtY/9BCcj13cV1D495zLr2HAveWDVVGJpLorkG5d674dtl +wD+B5EQIliCVR5GWMeciFGrewwKBgQDzfsU+EXlKAw6KMInyRP/+nWNk0PFir01H +Q4C/SrTM/Y8bCJ3/pWVAQsxEbQk1pOdWcdzHFf8BRncMA+OUDTxSCHJYiaqL+2pN +nNB3/bShocMKvDodJxXWMhdM2fMLFMtYCNsjr0DM8Cqvw7oZF8MY6oxM+uWzmI5R +nWKFMFXMyQKBgQDBK8PnKOSM69qJ7tgwUF827zUCNnOxvniIaTWPJOuFmZ/uIkIk +yCId6Ue892z82SPLacieBwQA6/bpPDTWXzszLDSCFoC0joqCAf6m1Vbt07iCl5P7 +xmLmZQAaLIW7hzgZ2JD4/hwDGklcWY1rYkic7dFwd8FxV1RKoR4pW4xnjQKBgQDf +nEbU9kUVg/MhUuwL8fPJxo3VstBKWUS1sjcU9S1Op3h5UhOPBzwRpIZkPGHdwr+0 +MkKXDgsuB6EiBpxDhVgk2Z7w0hQuE0gPWHhWCUaNvLkaLbuMtC0olL2zFOBPB9yp +zxA4GCSBT/lTioJnstu3EQahVzQFF49zQf6M49OXiQKBgCqOdwZjTH5gBnDSbWMM +WAFcxEzr5moG4nJzz/5sGqN5IRy1zDd/QkV2KEhjzWFbpGMgbgNTiLmz0BT6hUXl +/jS27B9AOPsdktyb88+ZuEfG6dYCmPnjBiOUrovbFk5IIAmiMAUT+W9HXN9shH0g +Ltxv392mcEGwmbfc1YJJfN2B +-----END PRIVATE KEY-----""" + + _CERTIFICATE = """-----BEGIN CERTIFICATE----- +MIIDADCCAegCCQCUu4mu6VfH/zANBgkqhkiG9w0BAQsFADBCMQswCQYDVQQGEwJE +RTELMAkGA1UEBwwCUEIxDDAKBgNVBAoMA1RTSTEYMBYGA1UEAwwPbXlmYWtlLnRl +c3QuY29tMB4XDTIwMDkwMTA5Mjc1M1oXDTIxMDkwMTA5Mjc1M1owQjELMAkGA1UE +BhMCREUxCzAJBgNVBAcMAlBCMQwwCgYDVQQKDANUU0kxGDAWBgNVBAMMD215ZmFr +ZS50ZXN0LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOu/D4J+ +REy03ogpAr5g0qGN1apQKdMjtipksOFmSgipy8rUXaaORygZc4CMKO8Zd//FffmP +R0YuAcp7cDA1pAM9FZ5zoO5OqH2QGr1DLutSzo1rznDBI3wUbvP1dG5ZH02ESDbi +esBNSv+TluIG0cqI/bbEj2dXCz4gShhUY46MnYI+31ezNpL+l03ptAtyE9Is4zlY +3oEcJWe7X1GdIoWoKvwCZjHYCqjfS4VMfLUSG9JfwKdFMc29aXupyIhKqI16deMh +nIJ7zSftUH4vNJubcd6owOwzR66rQ/m4+3LYDpS6j9OU9aWWzpmEVM/4BD0fvyQ9 +Zj2cqOXUlrgrSxsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAcNsm1y3PgC47O7qW +3X531EiXmXsKuFWrpQeuVgSI/PrtXCn3/Gr1GcFQDA3k5iyDsApohwbyUcpXhA6c +842r2Flb11tMF7lxHwHGffryBeFbvCNSNYDvN9zA/XQfqpYi4UPPXPyLH0jVD0Ek +BCqJJFFzkRbUTcvTxCUxNEYpIQC8U4RSyWXg5kTu6302YjmWaNcP3bfL4II/ddI4 +WyGW6tZI2z7GTYPutWljmtfgEto2Y3FimtiGU+P/uB6SxlESzkGEvAfEduGlyxY8 +uslYHnizLvYY6FaAdExE1TpM6YrM3b7aYMgv700CDsBCpFncQUx9tujpQxCmMoHZ +rNcviQ== +-----END CERTIFICATE-----""" + CERT_NAME = "SDK-" + uuid.uuid4().hex + + def setUp(self): + super(TestCertificate, self).setUp() + self.cert = self.client.create_certificate( + key=self._PRIVATE_KEY, + content=self._CERTIFICATE, + name=self.CERT_NAME + ) + + self.addCleanup(self.conn.waf.delete_certificate, self.cert) + + def test_list_certificates(self): + query = {} + certs = list(self.client.certificates(**query)) + self.assertGreaterEqual(len(certs), 0) + + def test_get_certificate(self): + cert = self.client.get_certificate(self.cert.id) + self.assertEqual(self.cert.name, cert.name) + self.assertEqual(self.cert.timestamp, cert.timestamp) + self.assertEqual(self.cert.expire_time, cert.expire_time) + + def test_find_certificate(self): + cert = self.client.find_certificate(self.cert.name) + self.assertEqual(self.cert.name, cert.name) + self.assertEqual(self.cert.timestamp, cert.timestamp) + self.assertEqual(self.cert.expire_time, cert.expire_time) + + def test_update_certificate(self): + cert2 = self.client.create_certificate( + key=self._PRIVATE_KEY, + content=self._CERTIFICATE, + name=self.CERT_NAME + "_2" + ) + + self.addCleanup(self.conn.waf.delete_certificate, cert2) + cert2_cmp = self.client.update_certificate( + cert2, + name=self.CERT_NAME + "_2_cp" + ) + self.assertEqual(cert2.name, cert2_cmp.name) + + cert2_cmp = self.client.get_certificate(cert2_cmp.id) + self.assertEqual(cert2.name, cert2_cmp.name) + self.assertEqual(cert2.id, cert2_cmp.id) diff --git a/otcextensions/tests/unit/sdk/waf/v1/test_proxy.py b/otcextensions/tests/unit/sdk/waf/v1/test_proxy.py index 28344e342..2bcdf66d3 100644 --- a/otcextensions/tests/unit/sdk/waf/v1/test_proxy.py +++ b/otcextensions/tests/unit/sdk/waf/v1/test_proxy.py @@ -11,6 +11,7 @@ # under the License. from otcextensions.sdk.waf.v1 import _proxy +from otcextensions.sdk.waf.v1 import certificate from openstack.tests.unit import test_proxy_base @@ -20,3 +21,29 @@ class TestWafProxy(test_proxy_base.TestProxyBase): def setUp(self): super(TestWafProxy, self).setUp() self.proxy = _proxy.Proxy(self.session) + + +class TestWafCertificate(TestWafProxy): + def test_certificate_create(self): + self.verify_create(self.proxy.create_certificate, + certificate.Certificate, + method_kwargs={'name': 'id'}, + expected_kwargs={'name': 'id', + 'prepend_key': False}) + + def test_certificate_delete(self): + self.verify_delete(self.proxy.delete_certificate, + certificate.Certificate, True) + + def test_certificate_find(self): + self.verify_find(self.proxy.find_certificate, certificate.Certificate) + + def test_certificate_get(self): + self.verify_get(self.proxy.get_certificate, certificate.Certificate) + + def test_certificates(self): + self.verify_list(self.proxy.certificates, certificate.Certificate) + + def test_certificate_update(self): + self.verify_update(self.proxy.update_certificate, + certificate.Certificate)