From 7d1285d4522d2f6f604a344679e4aca84449b955 Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Wed, 29 Nov 2017 20:41:44 -0800 Subject: [PATCH] Adds authn/root API endpoint (#1331) Issues: Fixes #1330 Problem: The authn/root API was not available. This API is really weird because its more like a fire-and-forget API. It provides no kind or selfLink in its return value. Analysis: Had to work around the resource deficiencies. Introduced our own kind and selfLink values because BIG-IP does not provide them. All methods except create are invalid. Tests: functional unit --- f5/bigip/shared/__init__.py | 2 + f5/bigip/shared/authn.py | 114 ++++++++++++++++++ f5/bigip/shared/test/functional/test_authn.py | 56 +++++++++ f5/bigip/shared/test/unit/test_authn.py | 65 ++++++++++ 4 files changed, 237 insertions(+) create mode 100644 f5/bigip/shared/authn.py create mode 100644 f5/bigip/shared/test/functional/test_authn.py create mode 100644 f5/bigip/shared/test/unit/test_authn.py diff --git a/f5/bigip/shared/__init__.py b/f5/bigip/shared/__init__.py index 75087f654..0b7f8cf84 100644 --- a/f5/bigip/shared/__init__.py +++ b/f5/bigip/shared/__init__.py @@ -16,6 +16,7 @@ # from f5.bigip.resource import OrganizingCollection +from f5.bigip.shared.authn import Authn from f5.bigip.shared.authz import Authz from f5.bigip.shared.file_transfer import File_Transfer from f5.bigip.shared.iapp import Iapp @@ -28,5 +29,6 @@ def __init__(self, mgmt): self._meta_data['allowed_lazy_attributes'] = [ File_Transfer, Iapp, + Authn, Authz ] diff --git a/f5/bigip/shared/authn.py b/f5/bigip/shared/authn.py new file mode 100644 index 000000000..c1d9224f5 --- /dev/null +++ b/f5/bigip/shared/authn.py @@ -0,0 +1,114 @@ +# coding=utf-8 +# +# Copyright 2017 F5 Networks Inc. +# +# 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 f5.bigip.resource import Collection +from f5.bigip.resource import OrganizingCollection +from f5.bigip.resource import Resource +from f5.sdk_exception import UnsupportedMethod +from f5.sdk_exception import UnsupportedOperation +from f5.sdk_exception import URICreationCollision + + +class Authn(OrganizingCollection): + def __init__(self, shared): + super(Authn, self).__init__(shared) + self._meta_data['allowed_lazy_attributes'] = [ + Roots + ] + + +class Roots(Collection): + def __init__(self, authn): + super(Roots, self).__init__(authn) + self._meta_data['allowed_lazy_attributes'] = [Root] + + def get_collection(self, **kwargs): + raise UnsupportedMethod( + "%s does not support get_collection" % self.__class__.__name__ + ) + + +class Root(Resource): + def __init__(self, roots): + super(Root, self).__init__(roots) + self._meta_data['required_json_kind'] = 'shared:authn:authrootitemstate' + + # The required parameters are a little vague. It turns out that the "user" + # value that is required is the "link" to the ID + self._meta_data['required_creation_parameters'] = {'oldPassword', 'newPassword'} + + def _create(self, **kwargs): + """wrapped by `create` override that in subclasses to customize""" + if 'uri' in self._meta_data: + error = "There was an attempt to assign a new uri to this "\ + "resource, the _meta_data['uri'] is %s and it should"\ + " not be changed." % (self._meta_data['uri']) + raise URICreationCollision(error) + self._check_exclusive_parameters(**kwargs) + requests_params = self._handle_requests_params(kwargs) + self._minimum_one_is_missing(**kwargs) + self._check_create_parameters(**kwargs) + kwargs = self._check_for_python_keywords(kwargs) + + # Reduce boolean pairs as specified by the meta_data entry below + for key1, key2 in self._meta_data['reduction_forcing_pairs']: + kwargs = self._reduce_boolean_pair(kwargs, key1, key2) + + # Make convenience variable with short names for this method. + _create_uri = self._meta_data['container']._meta_data['uri'] + session = self._meta_data['bigip']._meta_data['icr_session'] + + kwargs = self._prepare_request_json(kwargs) + + # Invoke the REST operation on the device. + response = session.post(_create_uri, json=kwargs, **requests_params) + + # Make new instance of self + result = self._produce_instance(response) + return result + + def _local_update(self, rdict): + super(Root, self)._local_update(rdict) + + # This API returns no kind, so we need to make our own + self.__dict__.update(dict(kind='shared:authn:authrootitemstate')) + + # This API returns no selfLink, so we need to make our own + tmos_version = self._meta_data['bigip']._meta_data['tmos_version'] + self_link = 'https://localhost/mgmt/shared/authn/root?ver={0}'.format(tmos_version) + self.__dict__.update(dict(selfLink=self_link)) + + def update(self, **kwargs): + raise UnsupportedOperation( + "%s does not support update" % self.__class__.__name__ + ) + + def load(self, **kwargs): + raise UnsupportedOperation( + "%s does not support load" % self.__class__.__name__ + ) + + def modify(self, **kwargs): + raise UnsupportedOperation( + "%s does not support modify" % self.__class__.__name__ + ) + + def delete(self, **kwargs): + raise UnsupportedOperation( + "%s does not support delete" % self.__class__.__name__ + ) diff --git a/f5/bigip/shared/test/functional/test_authn.py b/f5/bigip/shared/test/functional/test_authn.py new file mode 100644 index 000000000..20a3abcdc --- /dev/null +++ b/f5/bigip/shared/test/functional/test_authn.py @@ -0,0 +1,56 @@ +# Copyright 2017 F5 Networks Inc. +# +# 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 + +from distutils.version import LooseVersion + +pytestmark = pytest.mark.skipif( + LooseVersion(pytest.config.getoption('--release')) + < LooseVersion('12.0.0'), + reason='Needs v12 TMOS or greater to pass.' +) + + +@pytest.fixture(scope='function') +def root_credentials(mgmt_root): + result = mgmt_root.shared.authn.roots.root.create( + oldPassword='default', + newPassword='ChangeMyPassword1234' + ) + yield result + mgmt_root.shared.authn.roots.root.create( + oldPassword='ChangeMyPassword1234', + newPassword='default' + ) + + +@pytest.mark.skipif( + LooseVersion(pytest.config.getoption('--release')) >= LooseVersion('12.1.0'), + reason='This fixture requires < 12.1.0.' +) +class TestAuthnV12(object): + def test_create(self, root_credentials): + assert root_credentials.newPassword == 'ChangeMyPassword1234' + + +@pytest.mark.skipif( + LooseVersion(pytest.config.getoption('--release')) < LooseVersion('12.1.0'), + reason='This fixture requires >= 12.1.0.' +) +class TestAuthnPostV12(object): + def test_create(self, root_credentials): + assert root_credentials.generation == 0 diff --git a/f5/bigip/shared/test/unit/test_authn.py b/f5/bigip/shared/test/unit/test_authn.py new file mode 100644 index 000000000..2eea1930d --- /dev/null +++ b/f5/bigip/shared/test/unit/test_authn.py @@ -0,0 +1,65 @@ +# Copyright 2017 F5 Networks Inc. +# +# 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 f5.bigip.shared.authn import Root +from f5.bigip.shared.authn import Roots +from f5.sdk_exception import MissingRequiredCreationParameter +from f5.sdk_exception import UnsupportedMethod +from f5.sdk_exception import UnsupportedOperation + +import mock +import pytest + + +@pytest.fixture +def FakeAuthnRoot(): + mo = mock.MagicMock() + fake = Root(mo) + return fake + + +@pytest.fixture +def FakeAuthnRoots(): + mo = mock.MagicMock() + fake = Roots(mo) + return fake + + +class TestAuthnRoot(object): + def test_update_raises(self, FakeAuthnRoot): + with pytest.raises(UnsupportedOperation): + FakeAuthnRoot.update() + + def test_modify_raises(self, FakeAuthnRoot): + with pytest.raises(UnsupportedOperation): + FakeAuthnRoot.modify() + + def test_load_raises(self, FakeAuthnRoot): + with pytest.raises(UnsupportedOperation): + FakeAuthnRoot.load() + + def test_delete_raises(self, FakeAuthnRoot): + with pytest.raises(UnsupportedOperation): + FakeAuthnRoot.delete() + + def test_create_no_args(self, FakeAuthnRoot): + with pytest.raises(MissingRequiredCreationParameter): + FakeAuthnRoot.create() + + +class TestAuthnRoots(object): + def test_collection_raises(self, FakeAuthnRoots): + with pytest.raises(UnsupportedMethod): + FakeAuthnRoots.get_collection()