From d37b9ab19ff6f50b9c1746784623b3dd328ab525 Mon Sep 17 00:00:00 2001 From: Illes Gabor Date: Wed, 8 May 2019 16:29:13 +0200 Subject: [PATCH] Added seed code for access-management. Added seed code for access-management. Change-Id: I4c38a5edc8166fb8babf38458d255f0115b438ab Signed-off-by: gabor.illes --- .gitreview | 5 + LICENSE | 202 ++++++ access-management.spec | 80 +++ config-encoder-macros.spec | 45 ++ secrets/am-secrets.yaml | 17 + src/__init__.py | 15 + src/access_management/__init__.py | 15 + src/access_management/backend/__init__.py | 14 + src/access_management/backend/am_auth.py | 85 +++ src/access_management/backend/ambackend.py | 179 +++++ src/access_management/backend/authsender.py | 33 + src/access_management/backend/authserver.py | 99 +++ src/access_management/backend/restlogger.py | 50 ++ src/access_management/cli/__init__.py | 14 + src/access_management/cli/cli.py | 485 +++++++++++++ src/access_management/config/__init__.py | 14 + src/access_management/config/amconfigparser.py | 51 ++ src/access_management/config/defaults.py | 20 + src/access_management/cryptohelper/__init__.py | 14 + .../cryptohelper/decryptaaafiles.py | 51 ++ .../cryptohelper/encryptaaafiles.py | 46 ++ src/access_management/db/__init__.py | 14 + src/access_management/db/amdb.py | 793 +++++++++++++++++++++ src/access_management/rest-plugin/__init__.py | 14 + src/access_management/rest-plugin/am.ini | 16 + src/access_management/rest-plugin/am_api_base.py | 287 ++++++++ src/access_management/rest-plugin/permissions.py | 98 +++ src/access_management/rest-plugin/roles.py | 343 +++++++++ src/access_management/rest-plugin/roles_details.py | 105 +++ .../rest-plugin/roles_permissions.py | 196 +++++ src/access_management/rest-plugin/roles_users.py | 99 +++ src/access_management/rest-plugin/users.py | 411 +++++++++++ src/access_management/rest-plugin/users_details.py | 150 ++++ src/access_management/rest-plugin/users_keys.py | 171 +++++ src/access_management/rest-plugin/users_locks.py | 191 +++++ .../rest-plugin/users_owndetails.py | 140 ++++ .../rest-plugin/users_ownpasswords.py | 196 +++++ .../rest-plugin/users_parameters.py | 118 +++ .../rest-plugin/users_passwords.py | 159 +++++ src/access_management/rest-plugin/users_roles.py | 334 +++++++++ src/setup.py | 62 ++ systemd/auth-server.service | 26 + 42 files changed, 5457 insertions(+) create mode 100644 .gitreview create mode 100644 LICENSE create mode 100644 access-management.spec create mode 100644 config-encoder-macros.spec create mode 100644 secrets/am-secrets.yaml create mode 100644 src/__init__.py create mode 100644 src/access_management/__init__.py create mode 100644 src/access_management/backend/__init__.py create mode 100644 src/access_management/backend/am_auth.py create mode 100644 src/access_management/backend/ambackend.py create mode 100644 src/access_management/backend/authsender.py create mode 100644 src/access_management/backend/authserver.py create mode 100644 src/access_management/backend/restlogger.py create mode 100644 src/access_management/cli/__init__.py create mode 100644 src/access_management/cli/cli.py create mode 100644 src/access_management/config/__init__.py create mode 100644 src/access_management/config/amconfigparser.py create mode 100644 src/access_management/config/defaults.py create mode 100644 src/access_management/cryptohelper/__init__.py create mode 100644 src/access_management/cryptohelper/decryptaaafiles.py create mode 100644 src/access_management/cryptohelper/encryptaaafiles.py create mode 100644 src/access_management/db/__init__.py create mode 100644 src/access_management/db/amdb.py create mode 100644 src/access_management/rest-plugin/__init__.py create mode 100644 src/access_management/rest-plugin/am.ini create mode 100644 src/access_management/rest-plugin/am_api_base.py create mode 100644 src/access_management/rest-plugin/permissions.py create mode 100644 src/access_management/rest-plugin/roles.py create mode 100644 src/access_management/rest-plugin/roles_details.py create mode 100644 src/access_management/rest-plugin/roles_permissions.py create mode 100644 src/access_management/rest-plugin/roles_users.py create mode 100644 src/access_management/rest-plugin/users.py create mode 100644 src/access_management/rest-plugin/users_details.py create mode 100644 src/access_management/rest-plugin/users_keys.py create mode 100644 src/access_management/rest-plugin/users_locks.py create mode 100644 src/access_management/rest-plugin/users_owndetails.py create mode 100644 src/access_management/rest-plugin/users_ownpasswords.py create mode 100644 src/access_management/rest-plugin/users_parameters.py create mode 100644 src/access_management/rest-plugin/users_passwords.py create mode 100644 src/access_management/rest-plugin/users_roles.py create mode 100644 src/setup.py create mode 100644 systemd/auth-server.service diff --git a/.gitreview b/.gitreview new file mode 100644 index 0000000..b7d86ad --- /dev/null +++ b/.gitreview @@ -0,0 +1,5 @@ +[gerrit] +host=gerrit.att-akraino.org +port=29418 +project=rec/access-management +defaultremote=origin diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/access-management.spec b/access-management.spec new file mode 100644 index 0000000..20f813e --- /dev/null +++ b/access-management.spec @@ -0,0 +1,80 @@ +# Copyright 2019 Nokia + +# 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. + +Name: access-management +Version: %{_version} +Release: 1%{?dist} +Summary: Access Management +License: %{_platform_license} + +Vendor: %{_platform_vendor} +Source0: %{name}-%{version}.tar.gz +BuildArch: noarch +Requires: python-flask, python2-flask-restful, python2-configparser, mod_wsgi, python2-peewee +BuildRequires: python python-setuptools + +%description +This RPM contains Access Management component for Akraino REC blueprint + +%prep +%autosetup + +%install +mkdir -p %{buildroot}%{_python_site_packages_path}/access_management +mkdir -p %{buildroot}/var/log/access_management + +mkdir -p %{buildroot}%{_python_site_packages_path}/yarf/handlers/am +rsync -ra src/access_management/rest-plugin/* %{buildroot}/%{_python_site_packages_path}/yarf/handlers/am + +mkdir -p %{buildroot}/etc/required-secrets/ +cp secrets/am-secrets.yaml %{buildroot}/etc/required-secrets/am-secrets.yaml + +mkdir -p %{buildroot}%{_unitdir}/ +cp systemd/auth-server.service %{buildroot}%{_unitdir}/ + +cd src && python setup.py install --root %{buildroot} --no-compile --install-purelib %{_python_site_packages_path} --install-scripts %{_platform_bin_path} && cd - + + +%files +%defattr(0755,root,root) +%{_python_site_packages_path}/access_management* +%{_python_site_packages_path}/yarf/handlers/am/* +/etc/required-secrets/am-secrets.yaml +%dir %attr(0770, access-manager,access-manager) /var/log/access_management +%attr(0755,root, root) %{_platform_bin_path}/auth-server +%attr(0644,root, root) %{_unitdir}/auth-server.service + +%pre +/usr/bin/getent passwd access-manager > /dev/null||/usr/sbin/useradd -r access-manager + + +%post +if [ $1 -eq 2 ]; then + if [ -f %{{aaa_backend_config_path}} ]; then + sudo /usr/bin/systemctl restart auth-server + fi +fi + + +%preun + + +%postun +if [ $1 -eq 0 ]; then + rm -rf /opt/access_management + /usr/sbin/userdel access-manager +fi + +%clean +rm -rf %{buildroot} diff --git a/config-encoder-macros.spec b/config-encoder-macros.spec new file mode 100644 index 0000000..83c5d31 --- /dev/null +++ b/config-encoder-macros.spec @@ -0,0 +1,45 @@ +# Copyright 2019 Nokia + +# 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. + +Name: config-encoder-macros +Version: master.624ed05 +Release: 1%{?dist} +Summary: Helper macros for encoding config files +License: MIT +URL: https://github.com/picotrading/config-encoder-macros +Source0: http://purkki.dynamic.nsn-net.net/sources/github/picotrading/config-encoder-macros/config-encoder-macros-master.624ed05.tgz +Vendor: Jiri Tyr +BuildArch: noarch + +%define PKG_BASE_DIR /opt/config-encoder-macros + +%description +Set of Jinja2 and ERB macros which help to encode Python and Ruby data structure into a different file format + +%prep +%autosetup + +%build + +%install +mkdir -p %{buildroot}/%{PKG_BASE_DIR} +cp -r * %{buildroot}/%{PKG_BASE_DIR}/ + +%files +%license LICENSE.md +%defattr(0755,root,root) +%{PKG_BASE_DIR} + +%clean +rm -rf %{buildroot} diff --git a/secrets/am-secrets.yaml b/secrets/am-secrets.yaml new file mode 100644 index 0000000..65980b8 --- /dev/null +++ b/secrets/am-secrets.yaml @@ -0,0 +1,17 @@ +# Copyright 2019 Nokia + +# 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. + +# MariaDB Options +am_db_user_password: +am_db_user_backend_password: diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..287b513 --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2019 Nokia + +# 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__('pkg_resources').declare_namespace(__name__) diff --git a/src/access_management/__init__.py b/src/access_management/__init__.py new file mode 100644 index 0000000..287b513 --- /dev/null +++ b/src/access_management/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2019 Nokia + +# 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__('pkg_resources').declare_namespace(__name__) diff --git a/src/access_management/backend/__init__.py b/src/access_management/backend/__init__.py new file mode 100644 index 0000000..78c5878 --- /dev/null +++ b/src/access_management/backend/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2019 Nokia + +# 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. + diff --git a/src/access_management/backend/am_auth.py b/src/access_management/backend/am_auth.py new file mode 100644 index 0000000..7b3f94d --- /dev/null +++ b/src/access_management/backend/am_auth.py @@ -0,0 +1,85 @@ +# Copyright 2019 Nokia + +# 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 requests.exceptions import ConnectTimeout, ReadTimeout + +import yarf.restfullogger as logger +from yarf.authentication.base_auth import BaseAuthMethod +from access_management.backend.authsender import AuthSender +from yarf.restfulargs import RestConfig +from yarf.helpers import remove_secrets + + +class AMAuth(BaseAuthMethod): + def __init__(self): + super(AMAuth, self).__init__() + config = RestConfig() + config.parse() + conf = config.get_section("AM", format='dict') + self.logger = logger.get_logger() + try: + self.host = conf['host'] + self.port = conf['port'] + except KeyError as error: + self.logger.error("Failed to find all the needed parameters. Authentication with AM not possible: {}" + .format(str(error))) + self.sender = AuthSender(self.host, self.port) + + @staticmethod + def get_info(request): + splitted = request.full_path.split("/", 3) + domain = splitted[1] + domain_object = splitted[3].split("?")[0] + return domain, domain_object + + # Returns a touple: + # touple[0]: true if authenticated + # touple[1]: the username for this request + def get_authentication(self, request): + + try: + domain, domain_object = self.get_info(request) + method = request.method.upper() + except IndexError as error: + self.logger.error("Failed to get domain, object or method from request %s", str(error)) + return False, "" + + try: + token = request.headers.get("X-Auth-Token", type=str) + except KeyError: + self.logger.error("Failed to get the authentication token from request") + return False, "" + parameters = {'token': token, 'domain': domain, 'domain_object': domain_object, 'method': method} + username = '' + try: + response = self.sender.send_request(parameters) + self.logger.debug(response) + + if response['username'] != '': + username = response['username'] + if response.get('authorized', None) is not None: + if response['authorized']: + self.logger.info('User {} is authorized for accessing the given domain {}'.format(response[ + 'username'], remove_secrets(request.full_path))) + return True, username + elif username != '': + self.logger.info('User {} is not authorized for accessing the given domain {}'.format(response[ + 'username'], remove_secrets(request.full_path))) + else: + self.logger.info('Token({}) is not valid for accessing the given domain {}'.format(token, + remove_secrets(request.full_path))) + except (ConnectTimeout, ReadTimeout) as e: + self.logger.error('Failed to communicate with the authentication server. The following error occurred: {}'. + format(str(e))) + return False, username diff --git a/src/access_management/backend/ambackend.py b/src/access_management/backend/ambackend.py new file mode 100644 index 0000000..90e1402 --- /dev/null +++ b/src/access_management/backend/ambackend.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python + +# Copyright 2019 Nokia + +# 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. + +""" +ambackend module +Authorization backend of AM +""" +from keystoneauth1.identity import v3 +from keystoneauth1 import session +from keystoneclient.v3 import client +from keystoneclient.v3.tokens import TokenManager +from keystoneauth1.exceptions.http import Unauthorized, NotFound + +from access_management.db.amdb import AMDatabase, NotExist +import access_management.backend.restlogger as restlog +import access_management.config.defaults as defaults + + +class AMBackend(object): + """ + Authorization backend of AM + """ + def __init__(self, config): + """ + Creates an instance of the authorization module + Parses config and creates AMDB instance + """ + self.config = config + self.logger = restlog.get_logger(self.config) + + self.db = AMDatabase(db_name=self.config["DB"]["name"], db_addr=self.config["DB"]["addr"], + db_port=int(self.config["DB"]["port"]), db_user=self.config["DB"]["user"], + db_pwd=self.config["DB"]["pwd"], logger=self.logger) + + def is_authorized(self, token, domain="", domain_object="", method="", role_name=""): + """ + Does the authorization check + Validates token and extracts user_id, gets allowed endpoint+method from AMDB + + :param token: keystone token + :param domain: domian part of the endpoint of the request + :param domain_object: domain_object part of the endpoint of the request + :param method: method of the request + :returns: authorization result + :rtype: bool + """ + + if domain == "am" and domain_object == "users/ownpasswords": + return True, "" + + tokenmanager = self.make_auth(token) + username = "" + + try: + tokeninfo = tokenmanager.validate(token) + except Unauthorized as error: + self.logger.error("Failed to authenticate with given credentials: {}".format(str(error))) + return False, username + except NotFound: + self.logger.error("Unauthorized token") + return False, username + except Exception as error: + self.logger.error("Failure: {}".format(str(error))) + return False, username + + user_uuid = tokeninfo.user_id + username = tokeninfo.username + endpoint = {} + endpoint["name"] = domain+"/"+domain_object + + if endpoint["name"] != "/": + self.logger.debug("Endpoint checking") + try: + self.db.connect() + except Exception as error: + self.logger.error("Failure: {}".format(str(error))) + return False, username + try: + permissions = self.db.get_user_resources(user_uuid) + except Exception as error: + self.logger.error("Failure: {}".format(str(error))) + return False, username + finally: + try: + self.db.close() + except Exception as error: + self.logger.error("Failure: {}".format(str(error))) + return False, username + + + endpoint["splitted"] = endpoint["name"].split("/") + endpoint["length"] = len(endpoint["splitted"]) + for path in permissions: + per_result = self.check_permission(path, endpoint) + if per_result: + met_result = method in permissions[path] + if met_result: + self.logger.info("Endpoint authorization successful") + return True, username + else: + self.logger.error("Unauthorized request 1") + return False, username + else: + continue + + if role_name != "": + self.logger.debug("Role checking") + try: + self.db.connect() + except Exception as error: + self.logger.error("Failure: {}".format(str(error))) + return False, username + try: + permissions = self.db.get_user_roles(user_uuid) + except Exception as error: + self.logger.error("Failure: {}".format(str(error))) + return False, username + finally: + try: + self.db.close() + except Exception as error: + self.logger.error("Failure: {}".format(str(error))) + return False, username + + if role_name in permissions: + self.logger.info("Role name authorization successful") + return True, username + + self.logger.error("Unauthorized request 2") + return False, username + + def check_permission(self, key, endpoint): + """ + Checks the permission + + :param key: permission from the DB + :param endpoint: endpoint of the request + :returns: checking result + :rtype: bool + """ + key_splitted = key.split("/") + key_length = len(key_splitted) + if key_length == 1 and endpoint["splitted"][0] == key: + return True + if endpoint["length"] != key_length: + return False + for i in range(0, endpoint["length"]): + if key_splitted[i][0] == "<": + continue + if endpoint["splitted"][i] != key_splitted[i]: + return False + return True + + def make_auth(self, token): + """ + Makes a connection to Keystone for token validation + + :param token: keystone token + :returns: instance of keystone's TokenManager + :rtype: TokenManager + """ + auth = v3.Token(auth_url=self.config["Keystone"]["auth_uri"], token=token, project_name=defaults.PROJECT_NAME, project_domain_id="default") + sess = session.Session(auth=auth) + keystone = client.Client(session=sess) + tokenmanager = TokenManager(keystone) + return tokenmanager diff --git a/src/access_management/backend/authsender.py b/src/access_management/backend/authsender.py new file mode 100644 index 0000000..57a332b --- /dev/null +++ b/src/access_management/backend/authsender.py @@ -0,0 +1,33 @@ +# Copyright 2019 Nokia + +# 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 json +import requests + + +class AuthSender(object): + + def __init__(self, host, port): + self.url = "http://{0}:{1}/authorize/endpoint".format(host, port) + self.headers = {'content-type': 'application/json'} + self.counter = 0 + + def send_request(self, data): + payload = { + "method": 'post', + "params": json.dumps(data), + "id": self.counter, + } + self.counter += 1 + return requests.post(self.url, data=json.dumps(payload), headers=self.headers, timeout=10).json() diff --git a/src/access_management/backend/authserver.py b/src/access_management/backend/authserver.py new file mode 100644 index 0000000..e21fb17 --- /dev/null +++ b/src/access_management/backend/authserver.py @@ -0,0 +1,99 @@ +# Copyright 2019 Nokia + +# 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 json +import sys + +from flask import Flask, request +from flask_restful import Resource, Api +from access_management.backend.ambackend import AMBackend +from access_management.config.amconfigparser import AMConfigParser +import access_management.backend.restlogger as restlog +from werkzeug.exceptions import InternalServerError + +app = Flask(__name__) +api = Api(app) + + +class AuthorizeEndpoint(Resource): + def post(self): + backend = AMBackend(config) + params = json.loads(request.json['params']) + authorized, username = backend.is_authorized(token=params['token'], domain=params['domain'], + domain_object=params['domain_object'], method=params['method']) + return {'authorized': authorized, 'username': username} + + +class AuthorizeRole(Resource): + def post(self): + backend = AMBackend(config) + authorized, username = backend.is_authorized(token=request.json['token'], role_name=request.json['role']) + return {'authorized': authorized, 'username': username} + + +# class DumpTables(Resource): +# def get(self): +# backend = AMBackend(config) +# results = backend.dump_tables() +# return results + + +api.add_resource(AuthorizeEndpoint, '/authorize/endpoint') +api.add_resource(AuthorizeRole, '/authorize/role') +# api.add_resource(DumpTables, '/dumptables') + + +def main(): + global config + configparser = AMConfigParser("/etc/access_management/am_backend_config.ini") + config = configparser.parse() + logger = restlog.get_logger(config) + initialize(config,logger) + app.run(host=config["Api"]["host"], port=int(config["Api"]["port"]), debug=True) + + +def initialize(config, logger): + logger.info("Initializing...") + app.register_error_handler(Exception, handle_exp) + app.before_request(request_logger) + app.after_request(response_logger) + app.logger.addHandler(restlog.get_log_handler(config)) + logger.info("Starting up...") + + +def request_logger(): + app.logger.info('Request: remote_addr: %s method: %s endpoint: %s', request.remote_addr, request.method, + request.full_path) + + +def response_logger(response): + app.logger.info('Response: status: %s (Associated Request: remote_addr: %s, method: %s, endpoint: %s)', + response.status, request.remote_addr, request.method, request.full_path) + + app.logger.debug('Response\'s data: %s', response.data) + + return response + + +def handle_exp(failure): + app.logger.error("Internal error: %s ", failure) + raise InternalServerError() + + +if __name__ == '__main__': + try: + sys.exit(main()) + except Exception as error:# pylint: disable=broad-except + print "Failure: %s" % error + sys.exit(255) diff --git a/src/access_management/backend/restlogger.py b/src/access_management/backend/restlogger.py new file mode 100644 index 0000000..2d38140 --- /dev/null +++ b/src/access_management/backend/restlogger.py @@ -0,0 +1,50 @@ +# Copyright 2019 Nokia + +# 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 logging +from logging.handlers import RotatingFileHandler + +restlogger = None + +class RestLogger(object): + def __init__(self, config): + self.logger = logging.getLogger("AM") + self.logger.setLevel(config["Logging"]["loglevel"]) + self.filehandler = self._get_filehandler(config["Logging"]["logdir"]+"/am.log") + self.logger.addHandler(self.filehandler) + + @staticmethod + def _get_filehandler(filename): + rfh = RotatingFileHandler(filename, mode='a', maxBytes=1000000, backupCount=10, encoding=None, delay=0) + formatter = logging.Formatter('%(asctime)s:%(levelname)s:%(name)s:%(pathname)s:%(funcName)s:%(lineno)d:%(message)s') + rfh.setFormatter(formatter) + return rfh + + def get_logger(self): + return self.logger + + def get_handler(self): + return self.filehandler + +def get_logger(config): + global restlogger + if not restlogger: + restlogger = RestLogger(config) + return restlogger.get_logger() + +def get_log_handler(config): + global restlogger + if not restlogger: + restlogger = RestLogger(config) + return restlogger.get_handler() diff --git a/src/access_management/cli/__init__.py b/src/access_management/cli/__init__.py new file mode 100644 index 0000000..78c5878 --- /dev/null +++ b/src/access_management/cli/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2019 Nokia + +# 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. + diff --git a/src/access_management/cli/cli.py b/src/access_management/cli/cli.py new file mode 100644 index 0000000..15b27d9 --- /dev/null +++ b/src/access_management/cli/cli.py @@ -0,0 +1,485 @@ +# Copyright 2019 Nokia + +# 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. + +# pylint: disable=line-too-long, too-few-public-methods + +import sys +from copy import deepcopy +from hostcli.helper import ListerHelper, ShowOneHelper, CommandHelper +import getpass +import os + + +API_VERSION = 'v1' +RESOURCE_PREFIX = 'am/%s/' % API_VERSION +DEFAULTPROJECTID = 'default_project_id' +DOMAINID = 'domain_id' +ENABLED = 'enabled' +UUID = 'id' +USER = 'user' +OWNUUID = 'ownid' +USERS = 'users' +LINKS = 'links' +USERNAME = 'username' +NAME = 'name' +OPTIONS = 'options' +PASSWORDEXP = 'password_expires_at' +PASSWORD = 'password' +EMAIL = 'email' +ROLENAME = 'role_name' +ROLEDESC = 'desc' +ROLES = 'roles' +ISSERVICE = 'is_service' +ISCHROOT = 'is_chroot' +NEWPASSWORD = 'npassword' +OLDPASSWORD = 'opassword' +PROJECTID = 'project_id' +PROJECT = 'project' +RESOURCEPATH = 'res_path' +RESOURCEOP = 'res_op' +PERMISSIONNAME = 'permission_name' +PERMISSIONRES = 'resources' +PUBSSHKEY = 'key' +SORT = 'sort' + + +FIELDMAP = { + DEFAULTPROJECTID: {'display': 'Default-Project-ID', + 'help': 'The ID of the default project for the user.'}, + DOMAINID: {'display': 'Domain-ID', + 'help': 'The ID of the domain.'}, + ENABLED: {'display': 'Enabled', + 'help': 'Whether the user is able to log in or not.'}, + UUID: {'display': 'User-ID', + 'help': 'The user ID.'}, + USER: {'display': 'User', + 'help': 'The user ID, or user name.'}, + USERS: {'display': 'User-IDs', + 'help': 'List of the user IDs.'}, + LINKS: {'display': 'Links', + 'help': 'The links for the user resource.'}, + USERNAME: {'help': 'The user name.'}, + NAME: {'display': 'User-Name', + 'help': 'The user name.'}, + OPTIONS: {'display': 'Options', + 'help': 'Options'}, + PASSWORDEXP: {'display': 'Password-Expires', + 'help': 'The date and time when the password expires. The time zone is UTC. A null value indicates that the password never expires.'}, + PASSWORD: {'default': '', + 'help': 'The password'}, + EMAIL: {'display': 'E-mail', + 'help': 'The email'}, + ROLENAME: {'display': 'Role', + 'help': 'The role name.'}, + ROLEDESC: {'display': 'Description', + 'help': 'The description of the role. It should be enclosed in apostrophes if it contains spaces'}, + ROLES: {'display': 'Roles', + 'help': 'The roles of the user.'}, + ISSERVICE: {'display': 'Immutable', + 'help': 'Whether the role is a service role. It is non-modifiable.'}, + ISCHROOT: {'display': 'Log File Access right', + 'help': 'Permission to use chroot file transfer.'}, + NEWPASSWORD: {'default': '', + 'help': 'The new password.'}, + OLDPASSWORD: {'default': '', + 'help': 'The old password'}, + PROJECTID: {'help': 'The ID of the project'}, + PROJECT: {'help': 'The ID of the project'}, + RESOURCEPATH: {'help': 'Resource path is the corresponding REST API URL.'}, + RESOURCEOP: {'help': 'The resource operation'}, + PERMISSIONNAME: {'display': 'Permission-Name', + 'help': 'Existing operations for the REST API endpoint.'}, + PERMISSIONRES: {'display': 'Permission-Resources', + 'help': 'Path of the REST API endpoint.'}, + PUBSSHKEY: {'help': 'The public ssh key string itself (not a key file).'}, + SORT: {'help': 'Comma-separated list of sort keys and directions in the form of [:]. The direction defaults to ascending if not specified. ' + 'Sort keys are the case sensitive column names in the command output table. For this command they are: User-ID, User-Name, Enabled and Password-Expires.'} +} + +PASSWORDPOLICY_DOCSTRING = """ + The password must have a minimum length of 8 characters (maximum is 255 characters). + The allowed characters are lower case letters (a-z), upper case letters (A-Z), digits (0-9), and special characters (.,:;/(){}<>~\!?@#$%^&*_=+-). + The password must contain at least one upper case letter, one digit and one special character. + The new password is always checked against a password dictionary and it cannot be the same with any of the last 12 passwords already used.""" + + +def password_policy_docstring(a): + a.__doc__ = a.__doc__.replace("%PASSWORDPOLICY_DOCSTRING%", PASSWORDPOLICY_DOCSTRING) + return a + + +class AmCliLister(ListerHelper): + """Helper class for Lister""" + def __init__(self, app, app_args, cmd_name=None): + super(AmCliLister, self).__init__(app, app_args, cmd_name) + self.fieldmap = deepcopy(FIELDMAP) + self.resource_prefix = RESOURCE_PREFIX + + +class AmCliShowOne(ShowOneHelper): + """Helper class for ShowOne""" + def __init__(self, app, app_args, cmd_name=None): + super(AmCliShowOne, self).__init__(app, app_args, cmd_name) + self.fieldmap = deepcopy(FIELDMAP) + self.resource_prefix = RESOURCE_PREFIX + + +class AmCliCommand(CommandHelper): + """Helper class for Command""" + def __init__(self, app, app_args, cmd_name=None): + super(AmCliCommand, self).__init__(app, app_args, cmd_name) + self.fieldmap = deepcopy(FIELDMAP) + self.resource_prefix = RESOURCE_PREFIX + + +@password_policy_docstring +class CreateNewUser(AmCliCommand): + """A command for creating new user in keystone. + The password is prompted if not given as parameter. + %PASSWORDPOLICY_DOCSTRING%""" + def __init__(self, app, app_args, cmd_name=None): + super(CreateNewUser, self).__init__(app, app_args, cmd_name) + self.usebody = True + self.operation = 'post' + self.endpoint = 'users' + self.mandatory_positional = True + self.positional_count = 1 + self.arguments = [USERNAME, EMAIL, PASSWORD, PROJECT] + self.message = 'User created. The UUID is ##id' + + def take_action(self, parsed_args): + try: + if parsed_args.password == '': + password1 = getpass.getpass(prompt='Password: ') + password2 = getpass.getpass(prompt='Password again: ') + if password1 == password2: + parsed_args.password = password1 + else: + raise Exception('New passwords do not match') + result = self.send_receive(self.app, parsed_args) + if self.message: + self.app.stdout.write(ResetUserPassword.construct_message(self.message, result)) + except Exception as exp: + self.app.stderr.write('Failed with error %s\n' % str(exp)) + sys.exit(1) + + +class DeleteUsers(AmCliCommand): + """A command for deleting one or more existing users.""" + def __init__(self, app, app_args, cmd_name=None): + super(DeleteUsers, self).__init__(app, app_args, cmd_name) + self.operation = 'delete' + self.endpoint = 'users' + self.mandatory_positional = True + self.positional_count = 1 + self.arguments = [USER] + self.message = 'User deleted.' + + +class ListUsers(AmCliLister): + """A command for listing existing users.""" + def __init__(self, app, app_args, cmd_name=None): + super(ListUsers, self).__init__(app, app_args, cmd_name) + self.operation = 'get' + self.endpoint = 'users' + self.positional_count = 0 + self.arguments = [SORT] + self.columns = [UUID, NAME, ENABLED, PASSWORDEXP] + self.default_sort = [NAME, 'asc'] + + +@password_policy_docstring +class ChangeUserPassword(AmCliCommand): + """A command for changing the current user password (i.e. own password). + The old and new passwords are prompted if not given as parameter. + %PASSWORDPOLICY_DOCSTRING%""" + def __init__(self, app, app_args, cmd_name=None): + super(ChangeUserPassword, self).__init__(app, app_args, cmd_name) + self.usebody = True + self.operation = 'post' + self.endpoint = 'users/ownpasswords' + #self.mandatory_positional = False + self.no_positional = True + self.arguments = [OLDPASSWORD, NEWPASSWORD] + self.message = 'Your password has been changed.' + self.auth_required = False + + def take_action(self, parsed_args): + try: + if parsed_args.opassword == '': + parsed_args.opassword = getpass.getpass(prompt='Old password: ') + if parsed_args.npassword == '': + npassword1 = getpass.getpass(prompt='New password: ') + npassword2 = getpass.getpass(prompt='New password again: ') + if npassword1 == npassword2: + parsed_args.npassword = npassword1 + else: + raise Exception('New passwords do not match') + parsed_args.username = os.environ['OS_USERNAME'] + self.arguments.append(USERNAME) + result = self.send_receive(self.app, parsed_args) + if self.message: + self.app.stdout.write(ResetUserPassword.construct_message(self.message, result)) + except Exception as exp: + self.app.stderr.write('Failed with error %s\n' % str(exp)) + sys.exit(1) + + +@password_policy_docstring +class ResetUserPassword(AmCliCommand): + """A command for user administrators for changing other user's password. + Own password cannot be changed with this command. + Note that user management admin role is required. + The new password is prompted if not given as parameter. + %PASSWORDPOLICY_DOCSTRING%""" + def __init__(self, app, app_args, cmd_name=None): + super(ResetUserPassword, self).__init__(app, app_args, cmd_name) + self.usebody = True + self.operation = 'post' + self.endpoint = 'users/passwords' + self.mandatory_positional = True + self.positional_count = 1 + self.arguments = [USER, NEWPASSWORD] + self.message = 'Password has been reset for the user.' + + def take_action(self, parsed_args): + try: + if parsed_args.npassword == '': + npassword1 = getpass.getpass(prompt='New password: ') + npassword2 = getpass.getpass(prompt='New password again: ') + if npassword1 == npassword2: + parsed_args.npassword = npassword1 + else: + raise Exception('New passwords do not match') + result = self.send_receive(self.app, parsed_args) + if self.message: + self.app.stdout.write(ResetUserPassword.construct_message(self.message, result)) + except Exception as exp: + self.app.stderr.write('Failed with error %s\n' % str(exp)) + sys.exit(1) + + +class SetUserParameters(AmCliCommand): + """A command for setting user parameters.""" + def __init__(self, app, app_args, cmd_name=None): + super(SetUserParameters, self).__init__(app, app_args, cmd_name) + self.operation = 'post' + self.endpoint = 'users/parameters' + self.mandatory_positional = True + self.positional_count = 1 + self.arguments = [USER, PROJECTID, EMAIL] + self.message = 'Parameter of the user is changed.' + + +class ShowUserDetails(AmCliShowOne): + """A command for displaying the details of a user.""" + def __init__(self, app, app_args, cmd_name=None): + super(ShowUserDetails, self).__init__(app, app_args, cmd_name) + self.operation = 'get' + self.endpoint = 'users/details' + self.mandatory_positional = True + self.positional_count = 1 + self.arguments = [USER] + self.columns = [DEFAULTPROJECTID, DOMAINID, EMAIL, ENABLED, UUID, LINKS, NAME, OPTIONS, PASSWORDEXP, ROLES] + + +class ShowUserOwnDetails(AmCliShowOne): + """A command for displaying the details of a user.""" + def __init__(self, app, app_args, cmd_name=None): + super(ShowUserOwnDetails, self).__init__(app, app_args, cmd_name) + self.operation = 'get' + self.endpoint = 'users/owndetails' + self.mandatory_positional = True + self.positional_count = 0 + self.columns = [DEFAULTPROJECTID, DOMAINID, EMAIL, ENABLED, UUID, LINKS, NAME, OPTIONS, PASSWORDEXP, ROLES] + + +class AddRoleForUser(AmCliCommand): + """A command for adding role to a user.""" + def __init__(self, app, app_args, cmd_name=None): + super(AddRoleForUser, self).__init__(app, app_args, cmd_name) + self.operation = 'post' + self.endpoint = 'users/roles' + self.mandatory_positional = True + self.positional_count = 2 + self.arguments = [USER, ROLENAME] + self.message = 'Role has been added to the user.' + + +class RemoveRoleFromUser(AmCliCommand): + """A command for removing role from a user.""" + def __init__(self, app, app_args, cmd_name=None): + super(RemoveRoleFromUser, self).__init__(app, app_args, cmd_name) + self.operation = 'delete' + self.endpoint = 'users/roles' + self.mandatory_positional = True + self.positional_count = 2 + self.arguments = [USER, ROLENAME] + self.message = 'Role has been removed from the user.' + + +class LockUser(AmCliCommand): + """A command for locking an account.""" + def __init__(self, app, app_args, cmd_name=None): + super(LockUser, self).__init__(app, app_args, cmd_name) + self.operation = 'post' + self.endpoint = 'users/locks' + self.mandatory_positional = True + self.positional_count = 1 + self.arguments = [USER] + self.message = 'User has been locked.' + + +class UnlockUser(AmCliCommand): + """A command for enabling a locked account.""" + def __init__(self, app, app_args, cmd_name=None): + super(UnlockUser, self).__init__(app, app_args, cmd_name) + self.operation = 'delete' + self.endpoint = 'users/locks' + self.mandatory_positional = True + self.positional_count = 1 + self.arguments = [USER] + self.message = 'User has been enabled.' + + +class CreateNewRole(AmCliCommand): + """A command for creating a new role.""" + def __init__(self, app, app_args, cmd_name=None): + super(CreateNewRole, self).__init__(app, app_args, cmd_name) + self.operation = 'post' + self.endpoint = 'roles' + self.mandatory_positional = True + self.positional_count = 1 + self.arguments = [ROLENAME, ROLEDESC] + self.message = 'Role has been created.' + + +class ModifyRole(AmCliCommand): + """A command for modifying an existing role.""" + def __init__(self, app, app_args, cmd_name=None): + super(ModifyRole, self).__init__(app, app_args, cmd_name) + self.operation = 'put' + self.endpoint = 'roles' + self.mandatory_positional = True + self.positional_count = 1 + self.arguments = [ROLENAME, ROLEDESC] + self.message = 'Role has been modified.' + + +class DeleteRole(AmCliCommand): + """A command for deleting one or more existing roles.""" + def __init__(self, app, app_args, cmd_name=None): + super(DeleteRole, self).__init__(app, app_args, cmd_name) + self.operation = 'delete' + self.endpoint = 'roles' + self.mandatory_positional = True + self.positional_count = 1 + self.arguments = [ROLENAME] + self.message = 'Role has been deleted.' + + +class ListRoles(AmCliLister): + """A command for listing existing roles. Openstack roles won't be listed.""" + def __init__(self, app, app_args, cmd_name=None): + super(ListRoles, self).__init__(app, app_args, cmd_name) + self.operation = 'get' + self.endpoint = 'roles' + self.positional_count = 0 + self.arguments = [SORT] + self.columns = [ROLENAME, ROLEDESC, ISSERVICE, ISCHROOT] + self.default_sort = [ROLENAME, 'asc'] + + +class ShowRoleDetails(AmCliLister): + """A command for displaying the details of a role.""" + def __init__(self, app, app_args, cmd_name=None): + super(ShowRoleDetails, self).__init__(app, app_args, cmd_name) + self.operation = 'get' + self.endpoint = 'roles/details' + self.mandatory_positional = True + self.positional_count = 1 + self.arguments = [ROLENAME] + self.columns = [PERMISSIONNAME, PERMISSIONRES] + + +class ListUsersOfRole(AmCliLister): + """A command for listing the users of a role.""" + def __init__(self, app, app_args, cmd_name=None): + super(ListUsersOfRole, self).__init__(app, app_args, cmd_name) + self.operation = 'get' + self.endpoint = 'roles/users' + self.mandatory_positional = True + self.positional_count = 1 + self.arguments = [ROLENAME] + self.columns = [ROLENAME, USERS] + + +class AddPermissionToRole(AmCliCommand): + """A command for adding a new permission to a role.""" + def __init__(self, app, app_args, cmd_name=None): + super(AddPermissionToRole, self).__init__(app, app_args, cmd_name) + self.operation = 'post' + self.endpoint = 'roles/permissions' + self.mandatory_positional = True + self.positional_count = 3 + self.arguments = [ROLENAME, RESOURCEPATH, RESOURCEOP] + self.message = 'New permission added to role.' + + +class RemovePermissionFromRole(AmCliCommand): + """A command for removing a permission from a role.""" + def __init__(self, app, app_args, cmd_name=None): + super(RemovePermissionFromRole, self).__init__(app, app_args, cmd_name) + self.operation = 'delete' + self.endpoint = 'roles/permissions' + self.mandatory_positional = True + self.positional_count = 3 + self.arguments = [ROLENAME, RESOURCEPATH, RESOURCEOP] + self.message = 'Permission deleted from role.' + + +class ListPermissions(AmCliLister): + """A command for listing all the permissions and endpoints.""" + def __init__(self, app, app_args, cmd_name=None): + super(ListPermissions, self).__init__(app, app_args, cmd_name) + self.operation = 'get' + self.endpoint = 'permissions' + self.positional_count = 0 + self.arguments = [SORT] + self.columns = [PERMISSIONNAME, PERMISSIONRES] + self.default_sort = [PERMISSIONNAME, 'asc'] + + +class AddKey(AmCliCommand): + """A command for adding a public ssh key to a user.""" + def __init__(self, app, app_args, cmd_name=None): + super(AddKey, self).__init__(app, app_args, cmd_name) + self.operation = 'post' + self.endpoint = 'users/keys' + self.mandatory_positional = True + self.positional_count = 2 + self.arguments = [USER, PUBSSHKEY] + self.message = 'Key added to the user.' + + +class RemoveKey(AmCliCommand): + """A command for removing a public ssh key from a user.""" + def __init__(self, app, app_args, cmd_name=None): + super(RemoveKey, self).__init__(app, app_args, cmd_name) + self.operation = 'delete' + self.endpoint = 'users/keys' + self.mandatory_positional = True + self.positional_count = 1 + self.arguments = [USER] + self.message = 'Key removed from the user.' diff --git a/src/access_management/config/__init__.py b/src/access_management/config/__init__.py new file mode 100644 index 0000000..78c5878 --- /dev/null +++ b/src/access_management/config/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2019 Nokia + +# 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. + diff --git a/src/access_management/config/amconfigparser.py b/src/access_management/config/amconfigparser.py new file mode 100644 index 0000000..e33958b --- /dev/null +++ b/src/access_management/config/amconfigparser.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python + +# Copyright 2019 Nokia + +# 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. + +""" +amconfigparser module +Config parser for AM use +""" +import ConfigParser +import logging + +logger = logging.getLogger(__name__) + +class AMConfigParser(object): + """ + Config parser for AM + """ + cfg_file = '/etc/access_management/am_config.ini' + + + def __init__(self, config_file=""): + """ + Creates an instance of the config parser + """ + if config_file: + self.config_path = config_file + else: + self.config_path = self.cfg_file + + def parse(self): + """ + Parses the config + :return: returns dictionary with config + :rtype: dict[dict[str]] + """ + config = ConfigParser.ConfigParser() + config.read(self.config_path) + config_dict = {s: dict(config.items(s)) for s in config.sections()} + return config_dict diff --git a/src/access_management/config/defaults.py b/src/access_management/config/defaults.py new file mode 100644 index 0000000..39e43d6 --- /dev/null +++ b/src/access_management/config/defaults.py @@ -0,0 +1,20 @@ +# Copyright 2019 Nokia + +# 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. + +PROJECT_NAME = "infrastructure" +KS_MEMBER_NAME = "_member_" +KS_ADMIN_NAME = "admin" +AM_MEMBER_NAME = "basic_member" +INF_ADMIN_ROLE_NAME = "infrastructure_admin" +OS_ADMIN_ROLE_NAME = "openstack_admin" diff --git a/src/access_management/cryptohelper/__init__.py b/src/access_management/cryptohelper/__init__.py new file mode 100644 index 0000000..78c5878 --- /dev/null +++ b/src/access_management/cryptohelper/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2019 Nokia + +# 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. + diff --git a/src/access_management/cryptohelper/decryptaaafiles.py b/src/access_management/cryptohelper/decryptaaafiles.py new file mode 100644 index 0000000..4acf203 --- /dev/null +++ b/src/access_management/cryptohelper/decryptaaafiles.py @@ -0,0 +1,51 @@ +# Copyright 2019 Nokia + +# 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 M2Crypto +import os +import argparse +import json +import base64 + + +class DecryptAAAFile(object): + def __init__(self, pem_file): + self.key = M2Crypto.RSA.load_key(pem_file) + + def decrypt_file(self, encrypted_file_path): + with open(encrypted_file_path,'r') as encrypted_file: + jsoned_file = json.load(encrypted_file) + for user in jsoned_file["users"]: + if len(user[1]) != 0: + decoded_pass = base64.b64decode(user[1]) + decrypted_pass = self.key.private_decrypt(decoded_pass, M2Crypto.RSA.pkcs1_oaep_padding) + user[1] = decrypted_pass.decode('utf-32') + return jsoned_file + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument("key", help="Private key path") + parser.add_argument("file", help="Path of the file to decrypt") + args = parser.parse_args() + dec = DecryptAAAFile(args.key) + jsoned_file = dec.decrypt_file(args.file) + if args.file.endswith(".enc"): + decrypted_filename = args.file[:-4] + else: + decrypted_filename = args.file + ".dec" + decrypted_file = open(os.path.join(decrypted_filename), 'w') + decrypted_file.write(json.dumps(jsoned_file)) + decrypted_file.close() + print "Decrypting file {0} for AAA done. New file name: {1}".format(args.file, decrypted_filename) diff --git a/src/access_management/cryptohelper/encryptaaafiles.py b/src/access_management/cryptohelper/encryptaaafiles.py new file mode 100644 index 0000000..873907f --- /dev/null +++ b/src/access_management/cryptohelper/encryptaaafiles.py @@ -0,0 +1,46 @@ +# Copyright 2019 Nokia + +# 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 M2Crypto +import argparse +import json +import base64 + + +class EncryptAAAFile(object): + def __init__(self, pem_file): + self.key = M2Crypto.RSA.load_pub_key(pem_file) + + + def encrypt_file(self, file_path): + with open(file_path,'r') as file: + jsoned_file = json.load(file) + for user in jsoned_file["users"]: + encrypted_pass = self.key.public_encrypt(user[1], M2Crypto.RSA.pkcs1_oaep_padding) + encoded_pass = base64.b64encode(encrypted_pass) + user[1] = encoded_pass + encrypted_file_name = file_path + ".enc" + with open(encrypted_file_name, 'w') as encrypted_file: + encrypted_file.write(json.dumps(jsoned_file)) + return encrypted_file_name + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument("key", help="Public key path") + parser.add_argument("file", help="Path of the file to encrypt") + args = parser.parse_args() + enc = EncryptAAAFile(args.key) + encrypted_file_name = enc.encrypt_file(args.file) + print "Encrypting file {0} for AAA done. New file name: {1}".format(args.file, encrypted_file_name) diff --git a/src/access_management/db/__init__.py b/src/access_management/db/__init__.py new file mode 100644 index 0000000..78c5878 --- /dev/null +++ b/src/access_management/db/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2019 Nokia + +# 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. + diff --git a/src/access_management/db/amdb.py b/src/access_management/db/amdb.py new file mode 100644 index 0000000..46f6d2f --- /dev/null +++ b/src/access_management/db/amdb.py @@ -0,0 +1,793 @@ +#!/usr/bin/env python + +# Copyright 2019 Nokia + +# 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. + +""" +amdb module +Maintains AM database +""" + +from peewee import Model +from peewee import MySQLDatabase +from peewee import CharField, BooleanField, ForeignKeyField +from peewee import DoesNotExist + +AM_DB = MySQLDatabase(None) + + +class BaseAMModel(Model): + """ + Base model for AM database + """ + + class Meta(object): + database = AM_DB + + +class AMdbUser(BaseAMModel): + user_uuid = CharField(null=False, unique=True) + name = CharField(null=False, unique=True) + is_service = BooleanField(default=False) + email = CharField(default='') + + class Meta(object): + db_table = 'user' + + +class AMdbResource(BaseAMModel): + path = CharField(null=False) + op = CharField(null=False) + desc = CharField(default='') + + class Meta(object): + db_table = 'resource' + + +class AMdbRole(BaseAMModel): + name = CharField(null=False, unique=True) + is_service = BooleanField(default=False) + is_chroot = BooleanField(default=False) + desc = CharField(default='') + + class Meta(object): + db_table = 'role' + + +class AMdbRoleResource(BaseAMModel): + role_id = ForeignKeyField(AMdbRole, to_field='id', db_column='role_id') + res_id = ForeignKeyField(AMdbResource, to_field='id', db_column='res_id') + + class Meta(object): + db_table = 'role_resource' + + +class AMdbUserRole(BaseAMModel): + user_id = ForeignKeyField(AMdbUser, to_field='id', db_column='user_id') + role_id = ForeignKeyField(AMdbRole, to_field='id', db_column='role_id') + + class Meta(object): + db_table = 'user_role' + + +class NotExist(Exception): + def __init__(self, value): + self.value = value + + def __str__(self): + return repr(self.value) + + +class AlreadyExist(Exception): + def __init__(self, value): + self.value = value + + def __str__(self): + return repr(self.value) + + +class NotAllowedOperation(Exception): + def __init__(self, value): + self.value = value + + def __str__(self): + return repr(self.value) + + +class AMDatabase(object): + """ AM Database handler class """ + + def __init__(self, db_name, db_user, db_pwd, db_addr, db_port, logger, management_mode=False): + """ + Creates an instance of AM database + + :param db_name: Name of AM's MySQL database + :param db_user: Username of the MySQL user + :param db_pwd: Password of the MySQL user + :param db_addr: Address of the MySQL server + :param db_port: Port of the MySQL server (type int) + :param logger: Logger instance to be used + + :Example: + db = AMDatabase(db_name='am_database',db_addr='127.0.0.1', + db_port=3306, db_user='db_user',db_pwd='db_pwd', + logger=logging.getLogger("Example")) + db.connect() + # do your code here + db.close() + """ + self.db_name = db_name + self.db_user = db_user + self.db_pwd = db_pwd + self.db_host = db_addr + self.db_port = db_port + self.am_db = AM_DB + self.management_mode = management_mode + + if logger is None: + raise Exception("You did not give me a logger to use. That's a no-no!") + else: + self.logger = logger + + def connect(self): + """ + Connects to the database + :raise Exception on failure + """ + + try: + self.am_db.init(self.db_name, + host=self.db_host, port=self.db_port, + user=self.db_user, password=self.db_pwd) + self.am_db.connect() + self.logger.debug('Connected to database') + except Exception as ex: + self.logger.error('Error occured while connecting to database') + raise Exception('Error occured while connecting to database') + + def close(self): + """ + Closes the database connection + :raise Exception on failure + """ + + try: + self.am_db.close() + self.logger.debug('Database closed') + except Exception as ex: + self.logger.error('Error closing connection to database') + raise Exception('Error closing connection to database') + + def create_tables(self): + self.am_db.create_tables([AMdbUser, AMdbRole, AMdbResource, AMdbUserRole, AMdbRoleResource], safe=True) + # AMdbUser.create_table(safe=True) + # AMdbRole.create_table(safe=True) + # AMdbResource.create_table(safe=True) + # AMdbUserRole.create_table(safe=True) + # AMdbRoleResource.create_table(safe=True) + + def create_user(self, uuid, name, em='', service=False): + """ + Creates a user with a UUID and optional email parameter + + :param uuid: User identifier + :param name: User name + :param em: email + :param service: is_service parameter value + :return: returns AMdbUser instance + :rtype AMdbUser + :raise AlreadyExist on failure + """ + + self.logger.debug('Called DB function: create_user') + query = (AMdbUser.select().where((AMdbUser.user_uuid == uuid) | (AMdbUser.name == name))) + query.execute() + if query.namedtuples(): + raise AlreadyExist( + 'User already exists in table: {0}'.format(uuid)) + if service: + user = AMdbUser.create(user_uuid=uuid, name=name, is_service=self.management_mode, email=em) + else: + user = AMdbUser.create(user_uuid=uuid, name=name, is_service=False, email=em) + return user + + def get_user(self, uuid): + """ + Returns the user record based on user UUID + + :return: returns AMdbUser instance created + :raise NotExist on failure + """ + self.logger.debug('Called DB function: get_user') + try: + return AMdbUser.get(AMdbUser.user_uuid == uuid) + except DoesNotExist: + raise NotExist('User does not exist: {0}'.format(uuid)) + + def delete_user(self, uuid): + """ + Deletes user; also removes reference from other tables + + :param uuid: user id + :raise NotAllowedOperation if user is service user + """ + self.logger.debug('Called DB function: delete_user') + try: + user = self.get_user(uuid) + except NotExist as ex: + raise NotExist(ex) + + if user.is_service and not self.management_mode: + raise NotAllowedOperation( + 'Deleting service user is not allowed: {0}'.format(uuid)) + query = (AMdbUserRole + .delete().where(AMdbUserRole.user_id == user.id)) + query.execute() + query = (AMdbUser.delete().where(AMdbUser.user_uuid == uuid)) + query.execute() + + def set_user_param(self, uuid, email=''): + """ + Sets extra parameters for users + :param uuid: user identifier + :param email: email address of the user + :raise NotAllowedOperation if the user is a service user + """ + self.logger.debug('Called DB function: set_user_param') + try: + user = self.get_user(uuid) + except NotExist as ex: + raise NotExist(ex) + + if user.is_service and not self.management_mode: + raise NotAllowedOperation( + 'Modifying service user is not allowed: {0}'.format(uuid)) + query = (AMdbUser + .update({AMdbUser.email: email}) + .where(AMdbUser.user_uuid == uuid)) + query.execute() + + def get_all_users(self): + """ + Returns all of the users + + :return: Values are in a dict + :rtype: dict + """ + self.logger.debug('Called DB function: get_all_users') + query = (AMdbUser.select(AMdbUser.user_uuid, + AMdbUser.is_service, + AMdbUser.email)) + query.execute() + ret = {} + for user in query.namedtuples(): + ret[user.user_uuid] = {'user_uuid': user.user_uuid, + 'is_service': user.is_service, + 'email': user.email} + return ret + + def create_role(self, role_name, role_desc='', is_chroot=False): + """ + Creates role + + :param role_name: role name + :param role_desc: description of the role + :param is_chroot: is_chroot value + :return: returns AMdbRole instance created + :rtype: AMdbRole + :raise AlreadyExist if the role is already present + """ + self.logger.debug('Called DB function: create_role') + query = (AMdbRole.select().where(AMdbRole.name == role_name)) + query.execute() + if query.namedtuples(): + raise AlreadyExist( + 'Role already exists in table: {0}'.format(role_name)) + role = AMdbRole.create(name=role_name, desc=role_desc, is_chroot=is_chroot, is_service=self.management_mode) + return role + + def get_role(self, role_name): + """ + Gets role by role name + + :param role_name: role name + :return: AMdbRole instance + :rtype: AMdbRole + :raise NotExist if role not exist + """ + self.logger.debug('Called DB function: get_role') + try: + return AMdbRole.get(AMdbRole.name == role_name) + except DoesNotExist: + raise NotExist('Role does not exsist: {}'.format(role_name)) + + def delete_role(self, role_name): + """ + Deletes role by role name; + also removes from role_resource and user_role table + + :param role_name: role name + :raise NotAllowedOperation if role is service role + """ + self.logger.debug('Called DB function: delete_role') + role = self.get_role(role_name) + if role.is_service and not self.management_mode: + raise NotAllowedOperation( + 'Deleting service role is not allowed: {0}'.format(role_name)) + query = (AMdbUserRole + .delete().where(AMdbUserRole.role_id == role.id)) + query.execute() + query = (AMdbRoleResource + .delete().where(AMdbRoleResource.role_id == role.id)) + query.execute() + query = (AMdbRole.delete().where(AMdbRole.id == role.id)) + query.execute() + + def set_role_param(self, role_name, desc=None, is_chroot=False): + """ + Sets role optional parameters + + :param role_name: role name + :param desc: role description + :param is_chroot: is_chroot value + :raise NotAllowedOperation if role is service role + """ + self.logger.debug('Called DB function: set_role_param') + role = self.get_role(role_name) + if role.is_service and not self.management_mode: + raise NotAllowedOperation( + 'Modifying service role is not allowed: {0}'.format(role_name)) + query = (AMdbRole + .update({AMdbRole.desc: desc, AMdbRole.is_chroot: is_chroot, AMdbRole.is_service: self.management_mode}) + .where(AMdbRole.name == role_name)) + query.execute() + + def get_all_roles(self): + """ + Returns all roles in a dict + + :return: Values are in a dict + :rtype: dict + """ + self.logger.debug('Called DB function: get_all_roles') + roles = AMdbRole.select() + res = {} + for role in roles: + res[role.name] = {'role_name': role.name, + 'is_service': role.is_service, + 'desc': role.desc, + 'is_chroot': role.is_chroot} + return res + + def is_chroot_role(self, role_name): + """ + Checks if the role is a chroot role or not + + :param role_name: role name + :return: bool; true if role is chroot role + """ + self.logger.debug('Called DB function: is_chroot_role') + role = self.get_role(role_name) + return role.is_chroot + + def get_resource_with_operations(self, res_path): + """ + Gets resource by resource name + returns a dict: key is the resource path, + values are allowed operations in a list + In other words: returns the allowed operations on a resource + + :param res_path: resource path + :raise NotExist if the resource is not present + :returns dict where resource is the key, values are the operations + """ + self.logger.debug('Called DB function: get_resource_with_operations') + query = (AMdbResource.select().where(AMdbResource.path == res_path)) + query.execute() + res = dict() + if not query.namedtuples(): + raise NotExist('Resource does not exist: {0}'.format(res_path)) + for row in query.namedtuples(): + if row[1] not in res.keys(): + res[row[1]] = [row[2]] + else: + res[row[1]].append(row[2]) + return res + + def get_resource(self, res_path, res_op): + """ + Gets resource by resource name and path + + :param res_path: resource path + :param res_op: operation + :returns: AMdbResource instance + :rtype AMdbResource + :raise NotExist if resource is not present + """ + self.logger.debug('Called DB function: get_resource') + try: + return AMdbResource.get(AMdbResource.path == res_path, + AMdbResource.op == res_op) + except DoesNotExist: + raise NotExist('Resource {0} with op {1} does not exsist' + .format(res_path, res_op)) + + def get_resources(self): + """ + Gets all resources with operations + + :returns: dict[str:list[str]] values + :rtype dict + """ + self.logger.debug('Called DB function: get_resources') + resources = AMdbResource.select() + ret = dict() + for res in resources: + if res.path not in ret.keys(): + ret[res.path] = [res.op] + else: + ret[res.path].append(res.op) + return ret + + def add_user_role(self, uuid, role_name): + """ + Adds a role to a user + + :param uuid: user identifier + :param role_name: name of the role + :raise AlreadyExist if the user-role is already present, + NotAllowedOperation if the user is a service user + """ + + self.logger.debug('Called DB function: add_user_role') + try: + user = self.get_user(uuid) + except NotExist as ex: + raise NotExist(ex) + + if user.is_service and not self.management_mode: + raise NotAllowedOperation( + 'Service user roles cannot be modified: {0}'.format(uuid)) + role = self.get_role(role_name) + query = (AMdbUserRole.select() + .where(AMdbUserRole.user_id == user.id, + AMdbUserRole.role_id == role.id)) + query.execute() + if query.namedtuples(): + raise AlreadyExist('Role for user already exists in table: {0}:{1}' + .format(uuid, role_name)) + else: + query = (AMdbUserRole + .insert({AMdbUserRole.user_id: user.id, + AMdbUserRole.role_id: role.id})) + query.execute() + + def delete_user_role(self, uuid, role_name): + """ + Deletes role for a given user (removes permission). + Does not delete the role itself. + + :param uuid: user identifier + :param role_name: name of the role + :raise NotExist if the user has no such role, + NotAllowedOperation if the user is a service user + """ + self.logger.debug('Called DB function: delete_user_role') + try: + user = self.get_user(uuid) + except NotExist as ex: + raise NotExist(ex) + + if user.is_service and not self.management_mode: + raise NotAllowedOperation( + 'Service user roles cannot be modified: {0}'.format(uuid)) + role = self.get_role(role_name) + query = (AMdbUserRole + .delete().where(AMdbUserRole.user_id == user.id, + AMdbUserRole.role_id == role.id)) + ret = query.execute() + if ret == 0: + raise NotExist('User {0} has no role {1}.' + .format(user.user_uuid, role_name)) + + def get_user_roles(self, uuid): + """ + Gets role belonging to a user + + :param uuid: user identifier + :returns: list of roles for the user + :rtype: list[str] + """ + self.logger.debug('Called DB function: get_user_roles') + try: + user = self.get_user(uuid) + except NotExist as ex: + raise NotExist(ex) + + query = (AMdbUserRole.select() + .where(AMdbUserRole.user_id == user.id) + ).join(AMdbRole).where(AMdbRole.id == AMdbUserRole.role_id + ).select(AMdbRole.name) + res = [] + for row in query.namedtuples(): + res.append(row[0]) + return res + + def get_user_resources(self, uuid): + """ + Gets resources belonging to a user, returns a list of resources + returns a dict whith resource path as keys, + allowed operations as values in a list + + :param uuid: user identifier + :returns: a dict where the keys are resource names, + values are the operations in a list + :rtype: dict[str:list[str]] + """ + self.logger.debug('Called DB function: get_user_resources') + try: + user = self.get_user(uuid) + except NotExist as ex: + raise NotExist(ex) + + roles = AMdbUserRole.select().where( + AMdbUserRole.user_id == user.id) + res = dict({}) + for role_row in roles: + role_res = AMdbRoleResource.select().where( + AMdbRoleResource.role_id == role_row.role_id) + for r_res_row in role_res: + if r_res_row.res_id.path not in res.keys(): + res[r_res_row.res_id.path] = [r_res_row.res_id.op] + else: + res[r_res_row.res_id.path].append(r_res_row.res_id.op) + return res + + def add_resource_to_role(self, role_name, res_path, res_op): + """ + Assings a resource+operation to a role + + :param role_name: role name + :param res_path: resource path + :param res_op: resource operation + :raise AlreadyExist if there's such a pairing, + NotAllowedOperation if the role is a service role + """ + self.logger.debug('Called DB function: add_resource_to_role') + role = self.get_role(role_name) + if role.is_service and not self.management_mode: + raise NotAllowedOperation('Service role cannot be modified: {0}' + .format(role_name)) + res = self.get_resource(res_path, res_op) + query = (AMdbRoleResource.select().where( + AMdbRoleResource.role_id == role.id, + AMdbRoleResource.res_id == res.id)) + query.execute() + if query.namedtuples(): + raise AlreadyExist( + 'Role-resource already exists in table: {0}:{1}, {2}' + .format(role_name, res_path, res_op)) + else: + query = (AMdbRoleResource + .insert({AMdbRoleResource.role_id: role.id, + AMdbRoleResource.res_id: res.id})) + query.execute() + + def get_role_resources(self, role_name): + """ + Gets resources belonging to a role (like giving permission) + + :param role_name: role name + :returns: dictionary, where keys are resource paths and values are + operations in a list + :rtype: dict[str:list[str]] + """ + self.logger.debug('Called DB function: get_role_resources') + role = self.get_role(role_name) + query = (AMdbRoleResource.select() + .where(AMdbRoleResource.role_id == role.id) + .join(AMdbResource) + .where(AMdbResource.id == AMdbRoleResource.res_id) + ).select(AMdbResource.path, AMdbResource.op) + query.execute() + res = dict() + for row in query.namedtuples(): + if row[0] not in res.keys(): + res[row[0]] = [row[1]] + else: + res[row[0]].append(row[1]) + return res + + def delete_role_resource(self, role_name, res_path, res_op): + """ + Deletes a resource from a role (like removing permission) + :param role_name: role name + :param res_path: resource path + :param res_op: resource operation + """ + self.logger.debug('Called DB function: delete_role_resource') + role = self.get_role(role_name) + if role.is_service and not self.management_mode: + raise NotAllowedOperation('Service role cannot be modified: {0}' + .format(role_name)) + res = self.get_resource(res_path, res_op) + query = (AMdbRoleResource.delete() + .where(AMdbRoleResource.role_id == role.id, + AMdbRoleResource.res_id == res.id)) + ret = query.execute() + if not ret: + raise NotExist('Role {0} has no such resource:operation : {1}:{2}' + .format(role_name, res_path, res_op)) + + def create_resource(self, res_path, res_op, res_desc=''): + """ Creates resource """ + self.logger.debug('Called DB function: create_resource') + if self.management_mode: + q = (AMdbResource.select().where(AMdbResource.path == res_path, AMdbResource.op == res_op)) + q.execute() + if len(q.namedtuples()) > 0: + print 'Resource and operation already exists in table' + raise Exception(res_path+':'+res_op) + resource = AMdbResource.create(path=res_path, op=res_op, desc=res_desc) + return resource + + def update_resource(self, res_path, res_op, res_desc): + """ updates resource """ + self.logger.debug('Called DB function: update_resource') + if self.management_mode: + q = (AMdbResource.select().where(AMdbResource.path == res_path, AMdbResource.op == res_op)) + q.execute() + if len(q.namedtuples()) == 0: + print 'Resource does not exist' + raise Exception(res_path+":"+res_op) + q = (AMdbResource.update({AMdbResource.desc: res_desc}) + .where(AMdbResource.path == res_path, AMdbResource.op == res_op)) + q.execute() + + def get_user_uuid(self, name): + """ + Returns the user UUID based on user name + + :return: returns UUID + :raise NotExist on failure + """ + self.logger.debug('Called DB function: get_user_uuid') + try: + return AMdbUser.get(AMdbUser.name == name).user_uuid + except DoesNotExist: + raise NotExist('User does not exist: {0}'.format(name)) + + def get_user_name(self, uuid): + """ + Returns the user name based on user UUID + + :return: returns username + :raise NotExist on failure + """ + self.logger.debug('Called DB function: get_user_name') + try: + return AMdbUser.get(AMdbUser.user_uuid == uuid).name + except DoesNotExist: + raise NotExist('User does not exist: {0}'.format(uuid)) + + def get_role_users(self, role_name): + """ + Gets users associated to a given role + :param role_name: role name + :returns list containing uuids; if there are no users + associated to the role, an empty list + :rtype list[str] + :raise AlreadyExist if there's no such role + """ + self.logger.debug('Called DB function: get_role_users') + role = self.get_role(role_name) + query = (AMdbUserRole.select(AMdbUserRole.user_id) + .where(AMdbUserRole.role_id == role.id) + .join(AMdbUser).where(AMdbUser.id == AMdbUserRole.user_id)) + res = query.execute() + return [row.user_id.user_uuid for row in res] + + def get_user_table(self): + """ + Gets user table + :returns list of each row in dict format + :rtype list[dict] + """ + result = [] + query = AMdbUser.select().dicts() + for row in query: + result.append(row) + return result + + def get_role_table(self): + """ + Gets role table + :returns list of each row in dict format + :rtype list[dict] + """ + result = [] + query = AMdbRole.select().dicts() + for row in query: + result.append(row) + return result + + def get_resource_table(self): + """ + Gets resource table + :returns list of each row in dict format + :rtype list[dict] + """ + result = [] + query = AMdbResource.select().dicts() + for row in query: + result.append(row) + return result + + def get_user_role_table(self): + """ + Gets user_role table + :returns list of each row in dict where ids are replaced: + user_id -> user.name + role_id -> role.name + :rtype list[dict] + """ + result = [] + query = AMdbUserRole.select(AMdbUser.name.alias('user_name'), AMdbRole.name.alias('role_name'))\ + .join(AMdbUser).switch(AMdbUserRole).join(AMdbRole).dicts() + result = [row for row in query] + return result + + def get_role_resource_table(self): + """ + Gets role_resource table + :returns list of each row in dict where ids are replaced: + role_id -> role.name + res_id -> resource.path + resource.op + :rtype list[dict] + """ + result = [] + query = AMdbRoleResource.select(AMdbRole.name, AMdbResource.path, AMdbResource.op)\ + .join(AMdbRole).switch(AMdbRoleResource).join(AMdbResource).dicts() + result = [row for row in query] + return result + + def get_roles_for_permission(self, perm_name, op): + """ + Gets all roles where the permission is included + :returns list of role + perm_name -> resource.name + op -> resource.op + :rtype list[str] + """ + self.logger.debug('Called DB function: get_roles_for_permission') + result = [] + res_id = self.get_resource(perm_name, op).id + query = AMdbRoleResource.select(AMdbRole.name).join(AMdbRole).where(AMdbRoleResource.res_id == res_id).dicts() + for row in query: + result.append(row["name"]) + return result + + def get_all_role_perms(self): + """ + Gets all roles/permissions where the permission is included + :returns hashmap of permission, operation+roles + :rtype dict + """ + self.logger.debug('Called DB function: get_all_role_perms') + result = {} + res_lst = self.get_resources() + query = AMdbResource.select(AMdbResource.path, AMdbResource.op).dicts() + for row in query: + result[row["path"]+":"+row["op"]] = self.get_roles_for_permission(row["path"], row["op"]) + return result diff --git a/src/access_management/rest-plugin/__init__.py b/src/access_management/rest-plugin/__init__.py new file mode 100644 index 0000000..78c5878 --- /dev/null +++ b/src/access_management/rest-plugin/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2019 Nokia + +# 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. + diff --git a/src/access_management/rest-plugin/am.ini b/src/access_management/rest-plugin/am.ini new file mode 100644 index 0000000..a7c115d --- /dev/null +++ b/src/access_management/rest-plugin/am.ini @@ -0,0 +1,16 @@ +# Copyright 2019 Nokia + +# 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. + +[v1] +handlers=Users,UsersDetails,UserUnlock,UsersOwnpasswords,UsersRoles,Roles,RolesUsers,RolesDetails,UserLock,Permissions,RolesPermissions,UsersParameters,UsersPasswords,UsersKeys,UsersOwnDetails \ No newline at end of file diff --git a/src/access_management/rest-plugin/am_api_base.py b/src/access_management/rest-plugin/am_api_base.py new file mode 100644 index 0000000..b1b01d0 --- /dev/null +++ b/src/access_management/rest-plugin/am_api_base.py @@ -0,0 +1,287 @@ +# Copyright 2019 Nokia + +# 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 re +import os +import json +import traceback +import access_management.db.amdb as amdb +import yarf.restfullogger as logger +from cmframework.apis import cmclient +from keystoneauth1 import session +from keystoneauth1 import exceptions +from keystoneclient.v3 import client +from keystoneauth1.identity import v3 +from yarf.restresource import RestResource +from access_management.config.amconfigparser import AMConfigParser +import access_management.config.defaults as defaults + + +class AMApiBase(RestResource): + """ + The AMApiBase is the base class that all Access Management REST API endpoints should inherit form. It + implements some basic helper methods helping the handling of requests. + """ + + def __init__(self): + super(AMApiBase, self).__init__() + self.logger = logger.get_logger() + configparser = AMConfigParser() + self.config = configparser.parse() + self.db = amdb.AMDatabase(db_name=self.config["DB"]["name"], db_addr=self.config["DB"]["addr"], + db_port=int(self.config["DB"]["port"]), db_user=self.config["DB"]["user"], + db_pwd=self.config["DB"]["pwd"], logger=self.logger) + if self.get_token() != "": + self.keystone = self.auth_keystone() + self.token = self.get_token() + + @staticmethod + def error_handler(func): + def error_handled_function(*args, **kwargs): + try: + ret = func(*args, **kwargs) + return ret + except Exception as err: + traceback_info = traceback.format_exc() + return AMApiBase.construct_error_response( + 255, + "Server side error:{0}->{1}\nTraceback:\n{2}".format(err.__class__.__name__, + err.message, + traceback_info)) + return error_handled_function + + @staticmethod + def construct_error_response(code, description): + """ + Constructs an error response with the given code and message + :param code: + :param message: + :type code: int + :type description: str + :return: + """ + return AMApiBase.embed_data({}, code, description) + + @staticmethod + def embed_data(data, code=0, desc=""): + """ + Embeds the data into the NCIR Restfulframework preferred format. + :param data: The data to encapsulate, it should be a dictionary + :param code: The error code, it should be 0 on success. (Default: 0) + :param desc: The description of the error, if no error happened it can be an empty string. + :type data: dict + :type code: int + :type desc: str + :return: The encapsulated data as a dictionary. + :rtype: dict + """ + return {"code": code, + "description": desc, + "data": data} + + def parse_args(self): + """ + Helper function to handle special cases like all or an empty string + :return: The parsed arguments with all and empty string replaced with None + :rtype: dict + """ + args = self.parser.parse_args() + + for key, value in args.iteritems(): + if value == "all" or value == "": + args[key] = None + return args + + def get_user_from_uuid(self, uuid): + self.logger.debug("Start get_user_from_uuid") + try: + s_user = self.keystone.users.get(uuid) + except exceptions.http.NotFound as ex: + self.logger.error("{0}".format(ex)) + return 'None', defaults.PROJECT_NAME + except Exception as ex: + self.logger.error("{0}".format(ex)) + return 'None', defaults.PROJECT_NAME + + name = s_user.name + try: + project = s_user.default_project_id + except AttributeError: + project = None + + return name, project + + def id_validator(self, uuid): + if re.match("^[0-9a-f]+$", uuid) is not None: + return True + else: + return False + + def passwd_validator(self, passwd): + if (re.search(r"^(?=.*?[A-Z])(?=.*?[0-9])(?=.*?[][.,:;/(){}<>~\!?@#$%^&*_=+-])[][a-zA-Z0-9.,:;/(){}<>~\!?@#$%^&*_=+-]{8,255}$", passwd) is None): + return "The password must have a minimum length of 8 characters (maximum is 255 characters). The allowed characters are lower case letters (a-z), upper case letters (A-Z), digits (0-9), and special characters (][.,:;/(){}<>~\\!?@#$%^&*_=+-). The password must contain at least one upper case letter, one digit and one special character." + pwd_dict_check = os.system("echo '{0}' | cracklib-check | grep OK &>/dev/null".format(passwd)) + if pwd_dict_check != 0: + return "The password is incorrect: It cannot contain a dictionary word." + return None + + def get_role_id(self, role_name): + self.logger.debug("Start get_role_id") + try: + role_list = self.keystone.roles.list() + except Exception as ex: + self.logger.error("{0}".format(ex)) + return False, "{0}".format(ex) + + for role in role_list: + if role.name == role_name: + return str(role.id) + + def get_project_id(self, project_name): + self.logger.debug("Start get_project_id") + project_id = None + try: + project_list = self.keystone.projects.list() + except Exception: + return project_id + + for project in project_list: + if project.name == project_name: + return str(project.id) + + def get_uuid_from_token(self): + self.logger.debug("Start get_uuid_from_token") + try: + token_data = self.keystone.tokens.get_token_data(self.get_token()) + except exceptions.http.NotFound as ex: + self.logger.error("{0}".format(ex)) + return None + except Exception as ex: + self.logger.error("{0}".format(ex)) + return None + self.logger.debug({"Token owner": token_data["token"]["user"]["id"]}) + return token_data["token"]["user"]["id"] + + def send_role_request_and_check_response(self, role_id, user_id, method, proj_id): + try: + if method == "put": + self.keystone.roles.grant(role_id, user=user_id, project=proj_id) + elif method == "delete": + self.keystone.roles.revoke(role_id, user=user_id, project=proj_id) + else: + return False, "Not allowed method for role modification" + except Exception as ex: + self.logger.error("{0}".format(ex)) + return False, "{0}".format(ex) + + return True, "OK" + + def modify_role_in_keystone(self, role_name, user_id, method, project, need_admin_role = True): + um_proj_id = self.get_project_id(defaults.PROJECT_NAME) + if um_proj_id is None: + message = "The "+defaults.PROJECT_NAME+" project not found!" + self.logger.error(message) + return False, message + role_id = self.get_role_id(role_name) + if role_id is None: + message = "{} user role not found!".format(role_name) + self.logger.error(message) + return False, message + + state, message = self.send_role_request_and_check_response(role_id, user_id, method, um_proj_id) + if project and project != um_proj_id: + state, message = self.send_role_request_and_check_response(role_id, user_id, method, project) + + if need_admin_role and (role_name == defaults.INF_ADMIN_ROLE_NAME or role_name == defaults.OS_ADMIN_ROLE_NAME): + admin_role_id = self.get_role_id(defaults.KS_ADMIN_NAME) + if admin_role_id is None: + message = "The admin user role not found!" + self.logger.error(message) + return False, message + state, message = self.send_role_request_and_check_response(admin_role_id, user_id, method, um_proj_id) + if project and project != um_proj_id: + state, message = self.send_role_request_and_check_response(admin_role_id, user_id, method, project) + + return state, message + + def _close_db(self): + try: + self.db.close() + except Exception as err: + return False, err + return True, "DB closed" + + def _open_db(self): + try: + self.db.connect() + except Exception as err: + return False, err + return True, "DB opened" + + def check_chroot_linux_state(self, username, list_name, state): + cmc = cmclient.CMClient() + user_list = cmc.get_property(list_name) + user_list = json.loads(user_list) + self.logger.debug("Start the user list check") + self.logger.debug("Checked {0} user list : {1}".format(list_name, json.dumps(user_list))) + for val in user_list: + if val["name"] == username and val["state"] == state: + self.logger.debug("{0} checked!".format(username)) + return True + self.logger.debug("{0} failed to check!".format(username)) + return False + + def auth_keystone(self): + auth = v3.Token(auth_url=self.config["Keystone"]["auth_uri"], + token=self.get_token()) + sess = session.Session(auth=auth) + keystone = client.Client(session=sess) + return keystone + + def auth_keystone_with_pass(self, passwd, username=None, uuid=None): + if not username and not uuid: + return False + if username: + auth = v3.Password(auth_url=self.config["Keystone"]["auth_uri"], + username=username, + password=passwd, + project_name=defaults.PROJECT_NAME, + user_domain_id="default", + project_domain_id="default") + else: + auth = v3.Password(auth_url=self.config["Keystone"]["auth_uri"], + user_id=uuid, + password=passwd, + project_name=defaults.PROJECT_NAME, + user_domain_id="default", + project_domain_id="default") + sess = session.Session(auth=auth) + keystone = client.Client(session=sess) + return keystone + + def get_uuid_and_name(self, user): + try: + u_list = self.keystone.users.list() + except Exception as ex: + self.logger.error("{0}".format(ex)) + return False, "{0}".format(ex) + for element in u_list: + if user == element.id or user == element.name: + name = element.name + id = element.id + project = element.default_project_id + self.logger.debug("{0},{1},{2}".format(name, id, project)) + return True, {"name": name, "id": id, "project": project} + self.logger.error("{0} user does not exist in the keystone!".format(user)) + return False, {"{0} user does not exist in the keystone!".format(user)} diff --git a/src/access_management/rest-plugin/permissions.py b/src/access_management/rest-plugin/permissions.py new file mode 100644 index 0000000..ecb1f6c --- /dev/null +++ b/src/access_management/rest-plugin/permissions.py @@ -0,0 +1,98 @@ +# Copyright 2019 Nokia + +# 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 am_api_base import * + + +class Permissions(AMApiBase): + + """ + Permission list operations + + .. :quickref: Permissions;Permission list operations + + .. http:get:: /am/v1/permissions + + **Start Permission list** + + **Example request**: + + .. sourcecode:: http + + GET am/v1/permissions HTTP/1.1 + Host: haproxyvip:61200 + Accept: application/json + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + { + "code": 0, + "description": "" + "data": + { + "am/permissions": + { + "permission_name": "am/permissions", + "resources": ["GET"] + }, + "am/permissions/details": + { + "permission_name": "am/permissions/details", + "resources": ["GET"] + } + } + } + + :> json int code: the status code + :> json string description: the error description, present if code is non zero + :> json object data: a dictionary with the permissions elements + :> json string permission_name: Permission name + :> json string resources: permissions resources + """ + + endpoints = ['permissions'] + + def get(self): + self.logger.info("Received a permission list request!") + permissions_lis=dict({}) + state, permissions = self._permission_list() + + if state: + for element in permissions: + value = dict({}) + value.update({"permission_name": element, "resources": permissions[element]}) + permissions_lis.update({element: value}) + self.logger.info("The permission list response done!") + return AMApiBase.embed_data(permissions_lis, 0, "") + else: + return AMApiBase.construct_error_response(1, permissions) + + def _permission_list(self): + state_open, message_open = self._open_db() + if state_open: + try: + permissions = self.db.get_resources() + except Exception as ex: + self.logger.error("Internal error: {0}".format(ex)) + return False, "{0}".format(ex) + finally: + state_close, message_close = self._close_db() + if not state_close: + self._close_db() + return True, permissions + else: + return False, message_open diff --git a/src/access_management/rest-plugin/roles.py b/src/access_management/rest-plugin/roles.py new file mode 100644 index 0000000..d1bea48 --- /dev/null +++ b/src/access_management/rest-plugin/roles.py @@ -0,0 +1,343 @@ +# Copyright 2019 Nokia + +# 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 access_management.db.amdb as amdb +from am_api_base import * + + +class Roles(AMApiBase): + + """ + Role create operations + + .. :quickref: Roles;Role create operations + + .. http:post:: /am/v1/roles + + **Start Role create** + + **Example request**: + + .. sourcecode:: http + + POST am/v1/roles HTTP/1.1 + Host: haproxyvip:61200 + Accept: application/json + { + "role_name": "test_role" + "desc": "This is a test role" + } + + :> json string role_name: The created role name. + :> json string desc: A short description from the created role. + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + { + "code": 0, + "description": "Role created." + } + + :> json int code: the status code + :> json string description: the error description, present if code is non zero + + Role modify operations + + .. :quickref: Roles;Role modify operations + + .. http:put:: /am/v1/roles + + **Start Role modify** + + **Example request**: + + .. sourcecode:: http + + PUT am/v1/roles HTTP/1.1 + Host: haproxyvip:61200 + Accept: application/json + { + "role_name": "test_role" + "desc": "This is a test role" + } + + :> json string role_name: The modified role name. + :> json string desc: A short description from the modified role. + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + { + "code": 0, + "description": "Role modified." + } + + :> json int code: the status code + :> json string description: the error description, present if code is non zero + + Role delete operations + + .. :quickref: Roles;Role delete operations + + .. http:delete:: /am/v1/roles + + **Start Role delete** + + **Example request**: + + .. sourcecode:: http + + DELETE am/v1/roles HTTP/1.1 + Host: haproxyvip:61200 + Accept: application/json + { + "role_name": "test_role" + } + + :> json string role_name: The deleted role name. + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + { + "code": 0, + "description": "Role deleted." + } + + :> json int code: the status code + :> json string description: the error description, present if code is non zero + + Role list operations + + .. :quickref: Roles;Role list operations + + .. http:get:: /am/v1/roles + + **Start Role list** + + **Example request**: + + .. sourcecode:: http + + GET am/v1/roles HTTP/1.1 + Host: haproxyvip:61200 + Accept: application/json + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + { + "code": 0, + "description": "Role list." + "data": + { + "alarm_admin": + { + "desc": "Alarm Administrator", + "is_chroot": false, + "is_service": true, + "role_name": "alarm_admin" + }, + "alarm_viewer": + { + "desc": "Alarm Viewer", + "is_chroot": false, + "is_service": true, + "role_name": "alarm_viewer" + } + } + } + + :> json int code: the status code + :> json string description: the error description, present if code is non zero + :> json object data: a dictionary with the existing roles + :> json string role_name: The role name. + :> json string desc: The role description. + :> json string is_chroot: If this field is true, then this is a chroot user role. + :> json string is_service: If this field is true, then this is a service role and we created this role in deploymnet time. + """ + + endpoints = ['roles'] + parser_arguments = ['role_name', + 'desc'] + + def post(self): + self.logger.info("Received a role create request!") + args = self.parse_args() + if args["desc"] is None: + args["desc"] = "" + state, result = self._role_create(args) + + if state: + self.logger.info("The {0} role created!".format(args["role_name"])) + return AMApiBase.embed_data({}, 0, result) + else: + self.logger.error("The {0} role creation failed: {1}".format(args["role_name"], result)) + return AMApiBase.construct_error_response(1, result) + + def put(self): + self.logger.info("Received a role modify request!") + args = self.parse_args() + if args["desc"] is None: + args["desc"] = "" + state, result = self._role_modify(args) + + if state: + self.logger.info("The {0} role modified!".format(args["role_name"])) + return AMApiBase.embed_data({}, 0, result) + else: + self.logger.error("The {0} role modify failed: {1}".format(args["role_name"], result)) + return AMApiBase.construct_error_response(1, result) + + def get(self): + self.logger.info("Received a role list request!") + state, roles = self._role_list() + + if state: + self.logger.info("The role list response done!") + return AMApiBase.embed_data(roles, 0, "Role list.") + else: + self.logger.error("Role list creation failed: {0}".format(roles)) + return AMApiBase.construct_error_response(1, roles) + + def delete(self): + self.logger.info("Received a role delete request!") + args = self.parse_args() + + state, message = self._role_delete(args) + + if state: + self.logger.info("The {0} role deleted!".format(args["role_name"])) + return AMApiBase.embed_data({}, 0, message) + else: + self.logger.error("The {0} role deletion failed: {1}".format(args["role_name"], message)) + return AMApiBase.construct_error_response(1, message) + + def _role_modify(self, args): + state_open, message_open = self._open_db() + if state_open: + try: + self.db.set_role_param(args["role_name"], args["desc"]) + except amdb.NotAllowedOperation: + self.logger.error("Modifying service role is not allowed: {0}".format(args["role_name"])) + return False, "Modifying service role is not allowed: {0}".format(args["role_name"]) + except Exception as ex: + self.logger.error("Internal error: {0}".format(ex)) + return False, "Internal error: {0}".format(ex) + finally: + state_close, message_close = self._close_db() + if not state_close: + self._close_db() + return True, "Role modified." + else: + return False, message_open + + def _role_create(self, args): + state_open, message_open = self._open_db() + if state_open: + try: + self.db.create_role(args["role_name"], args["desc"]) + try: + self.keystone.roles.create(args["role_name"]) + except Exception as ex: + self.db.delete_role(args["role_name"]) + self.logger.error("Role {} already exists".format(args["role_name"])) + return False, "Role {} already exists".format(args["role_name"]) + except amdb.AlreadyExist: + self.logger.error("Role already exists in table: {0}".format(args["role_name"])) + return False, "Role already exists in table: {0}".format(args["role_name"]) + except Exception as ex: + self.logger.error("Internal error: {0}".format(ex)) + return False, "Internal error: {0}".format(ex) + finally: + state_close, message_close = self._close_db() + if not state_close: + self._close_db() + else: + return False, message_open + return True, "Role created." + + def _role_list(self): + state_open, message_open = self._open_db() + if state_open: + try: + roles = self.db.get_all_roles() + except Exception as ex: + self.logger.error("Internal error: {0}".format(ex)) + return False, "Internal error: {0}".format(ex) + finally: + state_close, message_close = self._close_db() + if not state_close: + self._close_db() + return True, roles + else: + return False, message_open + + def _add_roles_back_to_users(self, role_name): + uuid_list = self.db.get_role_users(role_name) + for uuid in uuid_list: + username, def_project = self.get_user_from_uuid(uuid) + state, message = self.modify_role_in_keystone(role_name, uuid, "put", def_project) + if not state: + return False, "Role deletion failed, please try again!" + return False, "Role deletion failed, try again" + + def _role_delete(self, args): + state_open, message_open = self._open_db() + if state_open: + try: + db_role = self.db.get_role(args["role_name"]) + if not db_role._data["is_service"]: + role_id = self.get_role_id(args["role_name"]) + if role_id is not None: + try: + self.keystone.roles.delete(role_id) + except Exception as ex: + self.logger.error("Some problem occured: {}".format(ex)) + return False, "Some problem occured: {}".format(ex) + + try: + self.db.delete_role(args["role_name"]) + except Exception: + try: + self.keystone.roles.create(args["role_name"]) + except Exception: + self.logger.error("Error during deleting role: {}".format(args["role_name"])) + return False, "Error during deleting role: {}".format(args["role_name"]) + state, message = self._add_roles_back_to_users(args["role_name"]) + return state, message + else: + raise amdb.NotAllowedOperation("") + except amdb.NotAllowedOperation: + self.logger.error("Deleting service role is not allowed: {0}".format(args["role_name"])) + return False, "Deleting service role is not allowed: {0}".format(args["role_name"]) + except Exception as ex: + self.logger.error("Internal error: {0}".format(ex)) + return False, "Internal error: {0}".format(ex) + finally: + state_close, message_close = self._close_db() + if not state_close: + self._close_db() + return True, "Role deleted." + else: + return False, message_open diff --git a/src/access_management/rest-plugin/roles_details.py b/src/access_management/rest-plugin/roles_details.py new file mode 100644 index 0000000..c12c738 --- /dev/null +++ b/src/access_management/rest-plugin/roles_details.py @@ -0,0 +1,105 @@ +# Copyright 2019 Nokia + +# 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 am_api_base import * + + +class RolesDetails(AMApiBase): + + """ + Role details operations + + .. :quickref: Roles details;Role details operations + + .. http:get:: /am/v1/roles/details + + **Start Role details** + + **Example request**: + + .. sourcecode:: http + + GET am/v1/roles/details HTTP/1.1 + Host: haproxyvip:61200 + Accept: application/json + { + "role_name": "test_role" + } + + :> json string role_name: The showed role name. + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + { + "code": 0, + "description": "Role details." + "data": + { + "has": + { + "permission_name": "has", + "resources": ["GET", "POST"] + } + } + } + + :> json int code: the status code + :> json string description: the error description, present if code is non zero + :> json object data: a dictionary with the role's details + :> json string permission_name: The permission's name. + :> json string resources: The permission resource's. + """ + + endpoints = ['roles/details'] + parser_arguments = ['role_name'] + + def get(self): + self.logger.info("Received a role show request!") + role_details=dict({}) + args = self.parse_args() + + state, details = self._role_show(args) + + if state: + if len(details) == 0: + role_details.update({"None":{"permission_name": "No permissions","resources": "None"}}) + else: + for perm in details: + role_details.update({perm: {"permission_name": perm,"resources": details[perm]}}) + self.logger.info("The {0} role show response done!".format(args["role_name"])) + return AMApiBase.embed_data(role_details, 0) + else: + self.logger.error("The {0} role show failed: {1}".format(args["role_name"], details)) + return AMApiBase.construct_error_response(1, details) + + def _role_show(self,args): + state_open, message_open = self._open_db() + if state_open: + try: + details = self.db.get_role_resources(args["role_name"]) + roles = self.db.get_role_table() + for role in roles: + if role["name"] == args["role_name"]: + break + except Exception as ex: + self.logger.error("Internal error: {0}".format(ex)) + return False, "Internal error: {0}".format(ex) + finally: + self.db.close() + return True, details + else: + return False, message_open diff --git a/src/access_management/rest-plugin/roles_permissions.py b/src/access_management/rest-plugin/roles_permissions.py new file mode 100644 index 0000000..c5a64d5 --- /dev/null +++ b/src/access_management/rest-plugin/roles_permissions.py @@ -0,0 +1,196 @@ +# Copyright 2019 Nokia + +# 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 am_api_base import * +import access_management.db.amdb as amdb + + +class RolesPermissions(AMApiBase): + + """ + Role add permission operations + + .. :quickref: Roles permission;Role add permission operations + + .. http:post:: /am/v1/roles/permissions + + **Start Role add permission** + + **Example request**: + + .. sourcecode:: http + + POST am/v1/roles/permissions HTTP/1.1 + Host: haproxyvip:61200 + Accept: application/json + { + "role_name": "test_role" + "res_path": "domain/domain_object" + "res_op": "GET" + } + + :> json string role_name: The role the permission gets to be added to. + :> json string res_path: The endpoint of the permission to be added. + :> json string res_op: The method of the permission to be added. + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + { + "code": 0, + "description": "Resource added to role" + } + + :> json int code: the status code + :> json string description: the error description, present if code is non zero + + Role remove permission operations + + .. :quickref: Roles permission;Role remove permission operations + + .. http:delete:: /am/v1/roles/permissions + + **Start Role remove permission** + + **Example request**: + + .. sourcecode:: http + + DELETE am/v1/roles/permissions HTTP/1.1 + Host: haproxyvip:61200 + Accept: application/json + { + "role_name": "test_role" + "res_path": "domain/domain_object" + "res_op": "GET" + } + + :> json string role_name: The role the permission gets to be removed from. + :> json string res_path: The endpoint of the permission to be removed. + :> json string res_op: The method of the permission to be removed. + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + { + "code": 0, + "description": "Resource removed from role" + } + + :> json int code: the status code + :> json string description: the error description, present if code is non zero + """ + + endpoints = ['roles/permissions'] + parser_arguments = ['role_name', + 'res_path', + 'res_op'] + + def post(self): + self.logger.info("Received a role add permission request!") + args = self.parse_args() + + state, permissions = self._add_permission(args) + + if state: + self.logger.info("The {1}:{2} permission added to {0} role!".format(args["role_name"], args["res_path"], args["res_op"])) + return AMApiBase.embed_data({}, 0, permissions) + else: + self.logger.error("The request to add permission {1}:{2} to role {0} failed: {3}".format(args["role_name"], args["res_path"], args["res_op"], permissions)) + return AMApiBase.embed_data({}, 1, permissions) + + def delete(self): + self.logger.info("Received a role remove permission request!") + args = self.parse_args() + + state, result = self._remove_permission(args) + + if state: + self.logger.info("The {1}:{2} permission removed from {0} role!".format(args["role_name"], args["res_path"], args["res_op"])) + return AMApiBase.embed_data({}, 0, result) + else: + self.logger.error("The request to remove permission {1}:{2} from role {0} failed: {3}".format(args["role_name"], args["res_path"], args["res_op"], result)) + return AMApiBase.construct_error_response(1, result) + + def _remove_permission(self, args): + state_open, message_open = self._open_db() + if state_open: + state_remove, message_remove = self._delete_role_resource(args) + if state_remove: + state_close, message_close = self._close_db() + if not state_close: + self._close_db() + return True, state_remove + else: + state_close, message_close = self._close_db() + if not state_close: + self._close_db() + return False, state_remove + else: + return False, message_open + + def _add_permission(self, args): + state_open, message_open = self._open_db() + if state_open: + state_add, message_add = self._add_resource_to_role(args) + if state_add: + state_close, message_close = self._close_db() + if not state_close: + self._close_db() + return True, message_add + else: + state_close, message_close = self._close_db() + if not state_close: + self._close_db() + return False, message_add + else: + return False, message_open + + def _add_resource_to_role(self, args): + try: + self.db.add_resource_to_role(args["role_name"], args["res_path"], args["res_op"]) + except amdb.AlreadyExist: + message = "Role-permission pair already exists in table: {0}:{1}, {2}".format(args["role_name"],args["res_path"],args["res_op"]) + self.logger.error(message) + return False, message + except amdb.NotAllowedOperation: + message = "Service role cannot be modified: {0}".format(args["role_name"]) + self.logger.error(message) + return False, message + except Exception as ex: + message = "Internal error: {0}".format(ex) + self.logger.error(message) + return False, message + return True, "Permission added to role!" + + def _delete_role_resource(self, args): + try: + self.db.delete_role_resource(args["role_name"],args["res_path"],args["res_op"]) + except amdb.NotExist: + message = "Role {0} has no such resource:operation: {1}:{2}".format(args["role_name"],args["res_path"],args["res_op"]) + self.logger.error(message) + return False, message + except amdb.NotAllowedOperation: + message = "Service role cannot be modified: {0}".format(args["role_name"]) + self.logger.error(message) + return False, message + except Exception as ex: + message = "Internal error: {0}".format(ex) + self.logger.error(message) + return False, message + return True, "Permission removed from role!" diff --git a/src/access_management/rest-plugin/roles_users.py b/src/access_management/rest-plugin/roles_users.py new file mode 100644 index 0000000..87176d0 --- /dev/null +++ b/src/access_management/rest-plugin/roles_users.py @@ -0,0 +1,99 @@ +# Copyright 2019 Nokia + +# 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 am_api_base import * + + +class RolesUsers(AMApiBase): + + """ + Role list users operations + + .. :quickref: Roles users;Role list users operations + + .. http:get:: /am/v1/roles/users + + **Start Role list users** + + **Example request**: + + .. sourcecode:: http + + GET am/v1/roles/permissions HTTP/1.1 + Host: haproxyvip:61200 + Accept: application/json + { + "role_name": "test_role" + } + + :> json string role_name: The role name to be searched in users. + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + { + "code": 0, + "description": "Resource added to role" + "data": + { + "has_all": + { + "role_name": "has_all", + "users": [user1, user2] + } + } + } + + :> json int code: the status code + :> json string description: the error description, present if code is non zero + :> json object data: a dictionary with the role name and role owners + :> json string role_name: The role name. + :> json string users: The role owners. + """ + + endpoints = ['roles/users'] + parser_arguments = ['role_name'] + + def get(self): + self.logger.info("Received a role users request!") + args = self.parse_args() + result=dict({}) + state, message = self._role_users(args) + + if state: + result.update({"role_name": args["role_name"]}) + result.update({"users": message}) + self.logger.info("The role users response done!") + return AMApiBase.embed_data({args["role_name"]:result}, 0, "These users have this role") + else: + self.logger.error("The {0} roles users list creation failed: {1}".format(args["role_name"], message)) + return AMApiBase.embed_data({}, 1, message) + + def _role_users(self, args): + state_open, message_open = self._open_db() + if state_open: + try: + users=self.db.get_role_users(args["role_name"]) + except Exception as ex: + self.logger.error("Internal error: {0}".format(ex)) + return False, "Internal error: {0}".format(ex) + finally: + state_close, message_close = self._close_db() + if not state_close: + self._close_db() + return True, users + else: + return False, message_open diff --git a/src/access_management/rest-plugin/users.py b/src/access_management/rest-plugin/users.py new file mode 100644 index 0000000..ec708c8 --- /dev/null +++ b/src/access_management/rest-plugin/users.py @@ -0,0 +1,411 @@ +# Copyright 2019 Nokia + +# 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 time +import access_management.db.amdb as amdb +from am_api_base import * +from keystoneauth1 import exceptions +from cmframework.apis import cmclient + + +class Users(AMApiBase): + + """ + User create operations + + .. :quickref: Users;User create operations + + .. http:post:: /am/v1/users + + **Start User create** + + **Example request**: + + .. sourcecode:: http + + POST am/v1/users HTTP/1.1 + Host: haproxyvip:61200 + Accept: application/json + { + "username": "user_1", + "password": "Passwd_1", + "email": "test@mail.com", + "project": "10f8fa2c6efe409d8207517128f03265", + "description": "desc" + } + + :> json string username: The created user name. + :> json string password: The user's password. + :> json string email: The user's e-mail. + :> json string project: ID of the project to be set as primary project for the user. + :> json string description: The user's description. + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + { + "code": 0, + "description": "", + "data": + { + "id": + } + } + + :> json int code: the status code + :> json string description: the error description, present if code is non zero + :> json object data: a dictionary with the created user's id + :> json string id: The created user's id. + + Users list operations + + .. :quickref: Users;Users list operations + + .. http:get:: /am/v1/users + + **Start Users list** + + **Example request**: + + .. sourcecode:: http + + GET am/v1/users HTTP/1.1 + Host: haproxyvip:61200 + Accept: application/json + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + { + "code": 0, + "description": "", + "data": + { + "0edf341a27544c349b7c37bb76ab25d1": + { + "enabled": true, + "id": "0edf341a27544c349b7c37bb76ab25d1", + "name": "cinder", + "password_expires_at": null + }, + "32e8859519f94b1ea80f61d53d17e74e": + { + "enabled": true, + "id": "32e8859519f94b1ea80f61d53d17e74e", + "name": "nova", + "password_expires_at": null + } + } + } + + :> json int code: the status code + :> json string description: the error description, present if code is non zero + :> json object data: The existing users. + :> json string enabled: The user's state. + :> json string id: The user's id. + :> json string name: The user's name. + :> json string password_expires_at: The user's password expiration date. + + User delete operations + + .. :quickref: Users;User delete operations + + .. http:delete:: /am/v1/users + + **Start User delete** + + **Example request**: + + .. sourcecode:: http + + DELETE am/v1/users HTTP/1.1 + Host: haproxyvip:61200 + Accept: application/json + { + "user": or + } + + :> json string user: The removed user's id or user name. + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + { + "code": 0, + "description": "User deleted!" + } + + :> json int code: the status code + :> json string description: the error description, present if code is non zero + """ + + endpoints = ['users'] + parser_arguments = ['username', + 'password', + 'email', + 'user', + 'project', + 'description'] + + def post(self): + self.logger.info("Received a user create request!") + args = self.parse_args() + + if args["email"] is not None: + if re.match("^[\.a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+\.[a-z]+$", args["email"]) is None: + self.logger.error("E-mail validation failed!") + return AMApiBase.embed_data({}, 1, "E-mail validation failed!") + + if self.id_validator(args["username"]): + self.logger.error("{0} username is invalid, because cannot assign a valid uuid to it.".format(args["username"])) + return AMApiBase.embed_data({}, 1, "{0} username is invalid, because cannot assign a valid uuid to it.".format(args["username"])) + + if args["project"]: + projectidstate = self.id_validator(args["project"]) + if projectidstate == False: + self.logger.error("Project id validation failed") + return AMApiBase.embed_data({}, 1, "Project id validation failed") + + if re.match("^[a-zA-Z0-9_-]+$", args["username"]) is None: + self.logger.error("Username validation failed!") + return AMApiBase.embed_data({}, 1, "Username validation failed!") + + passstate = self.passwd_validator(args["password"]) + if passstate is not None: + self.logger.error(passstate) + return AMApiBase.embed_data({}, 1, passstate) + + state, result = self._create_user(args) + if state: + self.logger.info("User created!") + return AMApiBase.embed_data({"id": result}, 0, "") + else: + return AMApiBase.embed_data({}, 1, result) + + def get(self): + self.logger.info("Received a user list request!") + user_list = {} + try: + self.keystone = self.auth_keystone() + u_list = self.keystone.users.list() + except Exception as ex: + self.logger.error("{0}".format(ex)) + return False, "{0}".format(ex) + + for element in u_list: + user_list.update({element.id : element._info}) + + self.logger.info("The user list response done!") + return AMApiBase.embed_data(user_list, 0, "User list.") + + def delete(self): + self.logger.info("Received a user delete request!") + args = self.parse_args() + + state, user_info = self.get_uuid_and_name(args["user"]) + if state: + token_owner = self.get_uuid_from_token() + if user_info["id"] == token_owner: + self.logger.error("The {0} user tried to delete own account!".format(user_info["id"])) + return AMApiBase.embed_data({}, 1, "You cannot delete your own account!") + + state, message = self._delete_user(user_info) + + if state: + self.logger.info("User deleted!") + return AMApiBase.embed_data({}, 0, "User deleted!") + else: + self.logger.error(message) + return AMApiBase.embed_data({}, 1, message) + else: + self.logger.error(user_info) + return AMApiBase.embed_data({}, 1, user_info) + + def _delete_user(self, user_info): + state, name = self._delete_user_from_db(user_info) + if state: + self.logger.info("User removed from the db!") + try: + self.keystone.users.delete(user_info["id"]) + except exceptions.http.NotFound as ex: + self.logger.info("{0} user does not exist in the keystone!".format(user_info["name"])) + return True, "Done, but this user didn't exist in the keystone!" + except Exception as ex: + self.logger.error("{0}".format(ex)) + return False, "{0}".format(ex) + return True, "Done" + else: + return False, name + + def _delete_user_from_db(self, user_info): + state_open, message_open = self._open_db() + if state_open: + try: + roles = self.db.get_user_roles(user_info["id"]) + + for role in roles: + if self.db.is_chroot_role(role): + self.logger.debug("This user has a chroot role.") + for x in range(3): + self.remove_chroot_linux_role_handling(user_info["id"], "Chroot", "cloud.chroot") + time.sleep(2) + if self.check_chroot_linux_state(user_info["name"], "cloud.chroot", "absent"): + self.db.delete_user(user_info["id"]) + return True, user_info["name"] + + if role == "linux_user": + self.logger.debug("This user has a linux_user role!") + for x in range(3): + self.remove_chroot_linux_role_handling(user_info["id"], "Linux", "cloud.linuxuser") + time.sleep(2) + if self.check_chroot_linux_state(user_info["name"], "cloud.linuxuser", "absent"): + self.db.delete_user(user_info["id"]) + return True, user_info["name"] + + self.db.delete_user(user_info["id"]) + except amdb.NotAllowedOperation: + self.logger.error("Deleting service user is not allowed: {0}".format(user_info["name"])) + return False, "Deleting service user is not allowed: {0}".format(user_info["name"]) + except amdb.NotExist: + self.logger.info("The {0} user does not exist!".format(user_info["name"])) + return True, "" + except Exception as ex: + self.logger.error("Internal error: {0}".format(ex)) + return False, "Internal error: {0}".format(ex) + finally: + state_close, message_close = self._close_db() + if not state_close: + self._close_db() + return True, user_info["name"] + else: + return False, message_open + + def _create_user(self, args): + roles = [] + ks_member_roleid = self.get_role_id(defaults.KS_MEMBER_NAME) + if ks_member_roleid is None: + self.logger.error("Member user role not found!") + return False, "Member user role not found!" + else: + roles.append(ks_member_roleid) + basic_member_roleid = self.get_role_id(defaults.AM_MEMBER_NAME) + if basic_member_roleid is None: + self.logger.error("basic_member user role not found!") + return False, "basic_member user role not found!" + else: + roles.append(basic_member_roleid) + + um_proj_id = self.get_project_id(defaults.PROJECT_NAME) + if um_proj_id is None: + self.logger.error("The user management project is not found!") + return False, "The user management project is not found!" + + if args["email"] is None: + args["email"] = 'None' + if args["project"] is None: + args["project"] = um_proj_id + + try: + c_user_out = self.keystone.users.create(name=args["username"], password=args["password"], email=args["email"], default_project=args["project"], description=args["description"]) + except exceptions.http.Conflict as ex: + self.logger.error("{0}".format(ex)) + return False, "This user exists in the keystone!" + except Exception as ex: + self.logger.error("{0}".format(ex)) + return False, "{0}".format(ex) + + ID = c_user_out.id + state, message = self._add_basic_roles(um_proj_id, ID, roles) + if not state: + return False, message + if args["project"] != um_proj_id: + state, message = self._add_basic_roles(args["project"], ID, [ks_member_roleid]) + if not state: + return False, message + return self._create_user_in_db(ID, args) + + def _add_basic_roles(self, project, ID, roles): + for role in roles: + try: + self.keystone.roles.grant(role, user=ID, project=project) + except Exception: + try: + self.keystone.roles.grant(role, user=ID, project=project) + except Exception as ex: + self.logger.error("{0}".format(ex)) + self.keystone.users.delete(ID) + return False, "{0}".format(ex) + return True, "OK" + + def _create_user_in_db(self, ID, args): + state_open, message_open = self._open_db() + if state_open: + try: + self.db.create_user(ID, args["username"]) + self.db.add_user_role(ID, defaults.AM_MEMBER_NAME) + except amdb.AlreadyExist as ex1: + self.logger.error("User already exists in table!") + try: + self.keystone.users.delete(ID) + self.db.delete_user(ID) + except amdb.NotAllowedOperation as ex2: + self.logger.error("Internal error: Except1: {0}, Except2: {1}".format(ex1, ex2)) + return False, "Except1: {0}, Except2: {1}".format(ex1, ex2) + except Exception as ex3: + self.logger.error("Internal error: Except1: {0}, Except2: {1}".format(ex1, ex3)) + return False, "Except1: {0}, Except2: {1}".format(ex1, ex3) + return False, "User already exists!" + except Exception as ex: + self.logger.error("Internal error: {0}".format(ex)) + try: + self.keystone.users.delete(ID) + except exceptions.http.NotFound as ex: + self.logger.error("{0}".format(ex)) + return False, "This user does not exist in the keystone!" + except Exception as ex: + self.logger.error("{0}".format(ex)) + return False, "{0}".format(ex) + return False, "Internal error: {0}".format(ex) + finally: + state_close, message_close = self._close_db() + if not state_close: + self._close_db() + return True, ID + else: + return False, message_open + + def remove_chroot_linux_role_handling(self, user_id, user_type, list_name): + cmc = cmclient.CMClient() + user_list = cmc.get_property(list_name) + user_list = json.loads(user_list) + self.logger.debug("{0} user list before the change: {1}".format(user_type, json.dumps(user_list))) + if user_list is not None: + self.logger.debug("The {0} user list exists!".format(user_type)) + username, def_project = self.get_user_from_uuid(user_id) + self.logger.debug("User name: {0}".format(username)) + for val in user_list: + if val["name"] == username: + val["public_key"] = "" + val["state"] = "absent" + val["remove"] = "yes" + val["password"] = "" + break + self.logger.debug("{0} user list after the change: {1}".format(user_type, json.dumps(user_list))) + cmc.set_property(list_name, json.dumps(user_list)) diff --git a/src/access_management/rest-plugin/users_details.py b/src/access_management/rest-plugin/users_details.py new file mode 100644 index 0000000..c3df794 --- /dev/null +++ b/src/access_management/rest-plugin/users_details.py @@ -0,0 +1,150 @@ +# Copyright 2019 Nokia + +# 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 access_management.db.amdb as amdb +from am_api_base import * +from keystoneauth1 import exceptions + + +class UsersDetails(AMApiBase): + + """ + User details operations + + .. :quickref: User details;User details operations + + .. http:get:: /am/v1/users/details + + **Start User details** + + **Example request**: + + .. sourcecode:: http + + GET am/v1/users/details HTTP/1.1 + Host: haproxyvip:61200 + Accept: application/json + { + "user": or + } + + :> json string user: The showed user's id or name. + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + { + "code": 0, + "description": "User details." + "data": + { + "616de2097d1647e88bdb83bfd9fdbedf": + { + "default_project_id": "5dfb6baff51a4e10ab98e262e6f3f59d", + "domain_id": "default", + "email": "None", + "enabled": true, + "id": "616de2097d1647e88bdb83bfd9fdbedf", + "links": + { + "self": "http://192.168.1.7:5000/v3/users/616de2097d1647e88bdb83bfd9fdbedf" + }, + "name": "um_admin", + "options": {}, + "password_expires_at": null, + "roles": [ "infrastructure_admin", "basic_member" ] + } + } + } + + :> json int code: the status code + :> json string description: the error description, present if code is non zero + :> json object data: the user details + :> json string default_project_id: The user's default project id. + :> json string domain_id: The user's domain id. + :> json string email: The user's e-mail. + :> json string enabled: The user's locking state. + :> json string id: The user's id. + :> json string links: The user's url address. + :> json string name: The user's name. + :> json string options: The user's options. + :> json string password_expires_at: The user's password expiration date. + :> json string roles: The user's roles. + """ + + endpoints = ['users/details'] + parser_arguments = ['user'] + + def get(self): + self.logger.info("Received a user show request!") + args = self.parse_args() + + state, user_info = self.get_uuid_and_name(args["user"]) + if state: + state, user_details = self.collect_user_details(user_info) + if state: + self.logger.info("User show response done!") + return AMApiBase.embed_data({user_info["id"]: user_details}, 0, "User details.") + else: + self.logger.error(user_details) + return AMApiBase.embed_data({}, 1, user_details) + else: + self.logger.error(user_info) + return AMApiBase.embed_data({}, 1, user_info) + + def collect_user_details(self, user_info): + try: + s_user = self.keystone.users.get(user_info["id"]) + except exceptions.http.NotFound as ex: + self.logger.error("{0}".format(ex)) + return False, "This user does not exist in the keystone!" + except Exception as ex: + self.logger.error("{0}".format(ex)) + return False, "{0}".format(ex) + + state, roles = self.ask_user_roles(user_info) + if state: + s_user = s_user._info + if 'email' not in s_user: + s_user["email"] = None + if 'description' not in s_user: + s_user["description"] = None + if roles == None: + s_user["roles"] = "The {0} user does not exist in the AM database!".format(user_info["name"]) + else: + s_user["roles"] = roles + return True, s_user + else: + return False, roles + + def ask_user_roles(self, user_info): + state_open, message_open = self._open_db() + if state_open: + try: + s_user_db = self.db.get_user_roles(user_info["id"]) + except amdb.NotExist: + self.logger.info ("The {0} user does not exist in the AM database!".format(user_info["id"])) + s_user_db = None + except Exception as ex: + self.logger.error("Internal error: {0}".format(ex)) + return False, ex + finally: + state_close, message_close = self._close_db() + if not state_close: + self._close_db() + return True, s_user_db + else: + return False, message_open diff --git a/src/access_management/rest-plugin/users_keys.py b/src/access_management/rest-plugin/users_keys.py new file mode 100644 index 0000000..24ee243 --- /dev/null +++ b/src/access_management/rest-plugin/users_keys.py @@ -0,0 +1,171 @@ +# Copyright 2019 Nokia + +# 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 am_api_base import * +from cmframework.apis import cmclient + + +class UsersKeys(AMApiBase): + + """ + User add key operations + + .. :quickref: User keys;User add key operations + + .. http:post:: /am/v1/users/keys + + **Start User add key** + + **Example request**: + + .. sourcecode:: http + + POST am/v1/users/keys HTTP/1.1 + Host: haproxyvip:61200 + Accept: application/json + { + "user": or + "key": + } + + :> json string user: The user's id or name. + :> json string key: The user's public key. + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + { + "code": 0, + "description": "User public key uploaded!" + } + + :> json int code: the status code + :> json string description: the error description, present if code is non zero + + User remove key operations + + .. :quickref: User keys;User remove key operations + + .. http:delete:: /am/v1/users/keys + + **Start User remove key** + + **Example request**: + + .. sourcecode:: http + + DELETE am/v1/users/keys HTTP/1.1 + Host: haproxyvip:61200 + Accept: application/json + { + "user": or + } + + :> json string user: The user's id or name. + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + { + "code": 0, + "description": "User public key removed!" + } + + :> json int code: the status code + :> json string description: the error description, present if code is non zero + """ + + endpoints = ['users/keys'] + parser_arguments = ['user', + 'key'] + + def post(self): + self.logger.info("Received an add key request!") + args = self.parse_args() + + if args["key"] is None: + self.logger.error("The public key is missing!") + return AMApiBase.embed_data({}, 1, "The public key is missing!") + + state, user_info = self.get_uuid_and_name(args["user"]) + if state: + state, message = self.user_checker(user_info, args["key"]) + if state: + self.logger.info("User public key uploaded!") + return AMApiBase.embed_data({}, 0, "User public key uploaded!") + else: + return AMApiBase.embed_data({}, 1, "Internal error: {0}".format(message)) + else: + self.logger.error(user_info) + return AMApiBase.embed_data({}, 1, user_info) + + def delete(self): + self.logger.info("Received a remove key request!") + args = self.parse_args() + + state, user_info = self.get_uuid_and_name(args["user"]) + if state: + state, message = self.user_checker(user_info, "") + if state: + self.logger.info("User public key removed!") + return AMApiBase.embed_data({}, 0, "User public key removed!") + else: + return AMApiBase.embed_data({}, 1, "Internal error: {0}".format(message)) + else: + self.logger.error(user_info) + return AMApiBase.embed_data({}, 1, user_info) + + def user_checker(self, user_info, key): + state_open, message_open = self._open_db() + if state_open: + try: + roles = self.db.get_user_roles(user_info["id"]) + self.logger.debug("Check the chroot role, when setting a user public key!") + for role in roles: + self.logger.debug("Role name: {0}".format(role)) + if self.db.is_chroot_role(role): + self.logger.debug("Found a chroot role attached to the {0} user!".format(user_info["name"])) + self.key_handler(user_info["name"], "Chroot", 'cloud.chroot', key) + + if role == "linux_user": + self.logger.debug("Found a Linux user role attached to the {0} user!".format(user_info["name"])) + self.key_handler(user_info["name"], "Linux", 'cloud.linuxuser', key) + except Exception as ex: + self.logger.error("Internal error: {0}".format(ex)) + return False, ex + finally: + state_close, message_close = self._close_db() + if not state_close: + self._close_db() + return True, "" + else: + return False, message_open + + def key_handler(self, username, user_type, list_name, key): + cmc = cmclient.CMClient() + user_list = cmc.get_property(list_name) + user_list = json.loads(user_list) + self.logger.debug("{0} user list before the change: {1}".format(user_type, json.dumps(user_list))) + if user_list: + self.logger.debug("The {0} user list exists!".format(user_type)) + for val in user_list: + if val["name"] == username: + val["public_key"] = key + break + self.logger.debug("{0} user list after the change: {1}".format(user_type, json.dumps(user_list))) + cmc.set_property(list_name, json.dumps(user_list)) diff --git a/src/access_management/rest-plugin/users_locks.py b/src/access_management/rest-plugin/users_locks.py new file mode 100644 index 0000000..081c834 --- /dev/null +++ b/src/access_management/rest-plugin/users_locks.py @@ -0,0 +1,191 @@ +# Copyright 2019 Nokia + +# 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 json +from am_api_base import * +from keystoneauth1 import exceptions +from cmframework.apis import cmclient + + +class UserLock(AMApiBase): + + """ + User lock operations + + .. :quickref: User lock;User lock operations + + .. http:post:: /am/v1/users/locks + + **Start User lock** + + **Example request**: + + .. sourcecode:: http + + POST am/v1/users/locks HTTP/1.1 + Host: haproxyvip:61200 + Accept: application/json + { + "user": or + } + + :> json string user: The locked user's id or name. + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + { + "code": 0, + "description": "User locked success." + } + + :> json int code: the status code + :> json string description: the error description, present if code is non zero + + User unlock operations + + .. :quickref: User lock;User unlock operations + + .. http:delete:: /am/v1/users/locks + + **Start User unlock** + + **Example request**: + + .. sourcecode:: http + + DELETE am/v1/users/locks HTTP/1.1 + Host: haproxyvip:61200 + Accept: application/json + { + "user": or + } + + :> json string user: The unlocked user's id or name. + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + { + "code": 0, + "description": "User unlocked!" + } + + :> json int code: the status code + :> json string description: the error description, present if code is non zero + """ + + endpoints = ['users/locks'] + parser_arguments = ['user'] + + def post(self): + self.logger.info("Received a user lock request!") + args = self.parse_args() + + state, user_info = self.get_uuid_and_name(args["user"]) + if state: + status, message = self._lock_user(user_info) + + if status: + self.logger.info("User {0} locked".format(user_info["name"])) + return AMApiBase.embed_data({}, 0, "User locked.") + else: + self.logger.error("User {0} lock failed: {1}".format(user_info["name"], message)) + return AMApiBase.embed_data({}, 1, message) + else: + self.logger.error(user_info) + return AMApiBase.embed_data({}, 1, user_info) + + def delete(self): + self.logger.info("Received a user unlock request!") + args = self.parse_args() + + state, user_info = self.get_uuid_and_name(args["user"]) + if state: + status, message = self._unlock_user(user_info) + + if status: + self.logger.info("User {0} unlocked!".format(user_info["name"])) + return AMApiBase.embed_data({}, 0, "User unlocked!") + else: + self.logger.error("User {0} unlock failed: {1}".format(user_info["name"], message)) + return AMApiBase.embed_data({}, 1, message) + else: + self.logger.error(user_info) + return AMApiBase.embed_data({}, 1, user_info) + + def _unlock_user(self, user_info): + try: + self.keystone.users.update(user_info["id"], enabled=True) + except exceptions.http.NotFound as ex: + self.logger.error("{0}".format(ex)) + return False, "This user does not exist in the keystone!" + except Exception as ex: + self.logger.error("{0}".format(ex)) + return False, "{0}".format(ex) + return self.user_checker(user_info, "-u") + + def _lock_user(self, user_info): + try: + self.keystone.users.update(user_info["id"], enabled=False) + except exceptions.http.NotFound as ex: + self.logger.error("{0}".format(ex)) + return False, "This user does not exist in the keystone!" + except Exception as ex: + self.logger.error("{0}".format(ex)) + return False, "{0}".format(ex) + return self.user_checker(user_info, "-l") + + def user_checker(self, user_info, user_state): + state_open, message_open = self._open_db() + if state_open: + try: + roles = self.db.get_user_roles(user_info["id"]) + self.logger.debug("Check the chroot role, when locking the user!") + for role in roles: + self.logger.debug("Role name: {0}".format(role)) + if self.db.is_chroot_role(role): + self.logger.debug("Found a chroot role attached to the {0} user!".format(user_info["name"])) + self.lock_state_handler(user_info["name"], "Chroot", "cloud.chroot", user_state) + if role == "linux_user": + self.logger.debug("Found a Linux role attached to the {0} user!".format(user_info["name"])) + self.lock_state_handler(user_info["name"], "Linux", "cloud.linuxuser", user_state) + except Exception as ex: + self.logger.error("Internal error: {0}".format(ex)) + return False, "Internal error: {0}".format(ex) + finally: + state_close, message_close = self._close_db() + if not state_close: + self._close_db() + return True, "" + else: + return False, message_open + + def lock_state_handler(self, username, user_type, list_name, state): + cmc = cmclient.CMClient() + user_list = cmc.get_property(list_name) + user_list = json.loads(user_list) + self.logger.debug("{0} user list before the change: {1}".format(user_type, json.dumps(user_list))) + if user_list is not None: + self.logger.debug("The {0} user list exists!".format(user_type)) + for val in user_list: + if val["name"] == username: + val["lock_state"] = state + break + self.logger.debug("{0} user list after the change: {1}".format(user_type, json.dumps(user_list))) + cmc.set_property(list_name, json.dumps(user_list)) diff --git a/src/access_management/rest-plugin/users_owndetails.py b/src/access_management/rest-plugin/users_owndetails.py new file mode 100644 index 0000000..30467da --- /dev/null +++ b/src/access_management/rest-plugin/users_owndetails.py @@ -0,0 +1,140 @@ +# Copyright 2019 Nokia + +# 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 access_management.db.amdb as amdb +from am_api_base import * +from keystoneauth1 import exceptions + + +class UsersOwnDetails(AMApiBase): + + """ + User own details operations + + .. :quickref: User own details;User own details operations + + .. http:get:: /am/v1/users/owndetails + + **Start User details** + + **Example request**: + + .. sourcecode:: http + + GET am/v1/users/details HTTP/1.1 + Host: haproxyvip:61200 + Accept: application/json + {} + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + { + "code": 0, + "description": "User own details." + "data": + { + "616de2097d1647e88bdb83bfd9fdbedf": + { + "default_project_id": "5dfb6baff51a4e10ab98e262e6f3f59d", + "domain_id": "default", + "email": "None", + "enabled": true, + "id": "616de2097d1647e88bdb83bfd9fdbedf", + "links": + { + "self": "http://192.168.1.7:5000/v3/users/616de2097d1647e88bdb83bfd9fdbedf" + }, + "name": "um_admin", + "options": {}, + "password_expires_at": null, + "roles": [ "infrastructure_admin", "basic_member" ] + } + } + } + + :> json int code: the status code + :> json string description: the error description, present if code is non zero + :> json object data: the user details + :> json string default_project_id: The user's default project id. + :> json string domain_id: The user's domain id. + :> json string email: The user's e-mail. + :> json string enabled: The user's locking state. + :> json string id: The user's id. + :> json string links: The user's url address. + :> json string name: The user's name. + :> json string options: The user's options. + :> json string password_expires_at: The user's password expiration date. + :> json string roles: The user's roles. + """ + + endpoints = ['users/owndetails'] + + def get(self): + self.logger.info("Received a show own details request!") + + id = self.get_uuid_from_token() + state, user_details = self.collect_user_details(id) + if state: + self.logger.info("User show own details response done!") + return AMApiBase.embed_data({id: user_details}, 0, "User own details.") + else: + self.logger.error(user_details) + return AMApiBase.embed_data({}, 1, user_details) + + def collect_user_details(self, id): + try: + s_user = self.keystone.users.get(id) + except exceptions.http.NotFound as ex: + self.logger.error("{0}".format(ex)) + return False, "You don't exist in the keystone!" + except Exception as ex: + self.logger.error("{0}".format(ex)) + return False, "{0}".format(ex) + + state, roles = self.ask_user_roles(id) + if state: + s_user = s_user._info + if 'email' not in s_user: + s_user["email"] = None + if 'description' not in s_user: + s_user["description"] = None + if roles == None: + s_user["roles"] = "You don't exist in the AM database!" + else: + s_user["roles"] = roles + return True, s_user + else: + return False, roles + + def ask_user_roles(self, id): + state_open, message_open = self._open_db() + if state_open: + try: + s_user_db = self.db.get_user_roles(id) + except amdb.NotExist: + self.logger.info ("You don't exist in the AM database!") + s_user_db = None + except Exception as ex: + self.logger.error("Internal error: {0}".format(ex)) + return False, ex + finally: + state_close, message_close = self._close_db() + if not state_close: + self._close_db() + return True, s_user_db + else: + return False, message_open diff --git a/src/access_management/rest-plugin/users_ownpasswords.py b/src/access_management/rest-plugin/users_ownpasswords.py new file mode 100644 index 0000000..1dbd2c2 --- /dev/null +++ b/src/access_management/rest-plugin/users_ownpasswords.py @@ -0,0 +1,196 @@ +# Copyright 2019 Nokia + +# 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 json +import crypt +import requests +import access_management.db.amdb as amdb +from am_api_base import * +from keystoneauth1 import exceptions +from cmframework.apis import cmclient + + +class UsersOwnpasswords(AMApiBase): + + """ + User set password operations + + .. :quickref: User ownpasswords;User set password operations + + .. http:post:: /am/v1/users/ownpasswords + + **Start User set password** + + **Example request**: + + .. sourcecode:: http + + POST am/v1/users/ownpasswords HTTP/1.1 + Host: haproxyvip:61200 + Accept: application/json + { + "npassword: "Passwd_1", + "opassword: "Passwd_2", + "username": "test_user" + } + + :> json string npassword: The user's new password + :> json string opassword: The user's old password + :> json string username: The user's username + :> json string id: The user's ID + Only one of username or id needs to be present. + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + { + "code": 0, + "description": "User password changed successfully!" + } + + :> json int code: the status code + :> json string description: the error description, present if code is non zero + """ + + endpoints = ['users/ownpasswords'] + parser_arguments = ['npassword', + 'opassword', + 'username', + 'id'] + FAILURE_RESPONSE = AMApiBase.embed_data({}, 1, "Password change failed!") + + def post(self): + self.logger.info("Received a set password request!") + args = self.parse_args() + + user = {} + state_open, message_open = self._open_db() + if state_open: + try: + if args["username"]: + user["name"] = args["username"] + user["uuid"] = self.db.get_user_uuid(args["username"]) + else: + user["uuid"] = args["id"] + user["name"] = self.db.get_user_name(args["id"]) + + try: + keystone = self.auth_keystone_with_pass(args["opassword"], user["name"]) + passstate = self.passwd_validator(args["npassword"]) + if passstate is not None: + self.logger.error(passstate) + return AMApiBase.embed_data({}, 1, passstate) + keystone.users.update_password(args["opassword"], args["npassword"]) + state = True + except exceptions.http.Unauthorized as ex: + if "password is expired" in ex.message: + passstate = self.passwd_validator(args["npassword"]) + if passstate is not None: + self.logger.error(passstate) + return AMApiBase.embed_data({}, 1, passstate) + state = self.change_password_with_request(args, user["uuid"]) + else: + self.logger.error("{0}".format(ex)) + state = False + except Exception as ex: + self.logger.error("{0}".format(ex)) + return self.FAILURE_RESPONSE + + if state: + state = self.set_ownpass_in_db(args, user) + if state: + self.logger.info("User password changed successfully!") + return AMApiBase.embed_data({}, 0, "User password changed successfully!") + else: + return self.FAILURE_RESPONSE + else: + return self.FAILURE_RESPONSE + + except amdb.NotExist as ex: + self.logger.error("User does not exist") + return self.FAILURE_RESPONSE + except Exception as ex: + self.logger.error("Internal error: {0}".format(ex)) + return self.FAILURE_RESPONSE + finally: + state_close, message_close = self._close_db() + if not state_close: + self._close_db() + else: + return self.FAILURE_RESPONSE + + def change_password_with_request(self, args, uuid): + url = self.config["Keystone"]["auth_uri"] + "/users/" + uuid + "/password" + parameter = {"user": {"password": args["npassword"], "original_password": args["opassword"]}} + header = {"Content-Type": "application/json"} + s_user_out = requests.post(url, data=json.dumps(parameter), headers=header, timeout=30) + + if s_user_out.status_code != 204: + s_user_out = s_user_out.json() + self.logger.error(s_user_out["error"]["message"]) + return False + return True + + def set_ownpass_in_db(self, args, user): + linux_user_role = False + chroot_user_role = False + state_open, message_open = self._open_db() + if state_open: + try: + roles = self.db.get_user_roles(user["uuid"]) + + for role in roles: + if self.db.is_chroot_role(role): + chroot_user_role = True + if role == "linux_user": + linux_user_role = True + + # if the user has a chroot or linux account, change the pwd of that also + if chroot_user_role: + self.linux_chroot_pass_handling("Chroot", "cloud.chroot", args["npassword"], user["name"]) + if linux_user_role: + self.linux_chroot_pass_handling("Linux", "cloud.linuxuser", args["npassword"], user["name"]) + + except amdb.NotExist as ex: + self.logger.error("User does not exist") + return False + except Exception as ex: + self.logger.error("Internal error: {0}".format(ex)) + return False + finally: + state_close, message_close = self._close_db() + if not state_close: + self._close_db() + else: + self.logger.error("Could not open DB") + return False + + return True + + def linux_chroot_pass_handling(self, user_type, list_name, passwd, username): + cmc = cmclient.CMClient() + user_list = cmc.get_property(list_name) + user_list = json.loads(user_list) + self.logger.debug("{0} user list before the change: {1}".format(user_type, json.dumps(user_list))) + if user_list is not None: + self.logger.debug("The {0} user list exists!".format(user_type)) + self.logger.debug("Username: {0}".format(username)) + for val in user_list: + if val["name"] == username: + val["password"] = crypt.crypt(passwd, crypt.mksalt(crypt.METHOD_SHA512)) + break + self.logger.debug("{0} user list after the change: {1}".format(user_type, json.dumps(user_list))) + cmc.set_property(list_name, json.dumps(user_list)) diff --git a/src/access_management/rest-plugin/users_parameters.py b/src/access_management/rest-plugin/users_parameters.py new file mode 100644 index 0000000..37b2d6c --- /dev/null +++ b/src/access_management/rest-plugin/users_parameters.py @@ -0,0 +1,118 @@ +# Copyright 2019 Nokia + +# 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 am_api_base import * +from keystoneauth1 import exceptions + +class UsersParameters(AMApiBase): + + """ + User set parameter operations + + .. :quickref: User set parameter;User set parameter operations + + .. http:post:: /am/v1/users/parameters + + **Start User set parameter** + + **Example request**: + + .. sourcecode:: http + + POST am/v1/users/parameters HTTP/1.1 + Host: haproxyvip:61200 + Accept: application/json + { + "user": or + "project_id: + "email": + } + + :> json string user: The user's id or name. + :> json string project_id: The user's default project id. + :> json string email: The user's email. + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + { + "code": 0, + "description": "User parameter modified." + } + + :> json int code: the status code + :> json string description: the error description, present if code is non zero + """ + + endpoints = ['users/parameters'] + parser_arguments = ['user', + 'project_id', + 'email', + 'description'] + + def post(self): + self.logger.info("Received a set parameters request!") + args = self.parse_args() + + if args["project_id"] is not None: + projectidstate = self.id_validator(args["project_id"]) + if projectidstate == False: + self.logger.error("Project id validation failed") + return AMApiBase.embed_data({}, 1, "Project id validation failed") + + state, user_info = self.get_uuid_and_name(args["user"]) + if state: + status, message = self._set_params(args, user_info) + + if status: + self.logger.info("User parameter modified.") + return AMApiBase.embed_data({}, 0, "") + else: + self.logger.error("Internal error in the keystone part: {0}".format(message)) + return AMApiBase.embed_data({}, 1, message) + else: + self.logger.error(user_info) + return AMApiBase.embed_data({}, 1, user_info) + + def _set_params(self, args, user_info): + if args["project_id"] is not None: + um_proj_id = self.get_project_id(defaults.PROJECT_NAME) + if um_proj_id is None: + self.logger.error("The user management project is not found!") + return False, "Keystone error, please try again." + ks_member_roleid = self.get_role_id(defaults.KS_MEMBER_NAME) + if ks_member_roleid is None: + self.logger.error("Member user role not found!") + return False, "Keystone error, please try again." + if args["project_id"] != um_proj_id and user_info["project"] != args["project_id"]: + state, message = self.send_role_request_and_check_response(ks_member_roleid, user_info["id"], "put", args["project_id"]) + if not state: + self.logger.error("KS error adding project: {0}".format(message)) + return False, "Keystone error, please try again." + if user_info["project"] and user_info["project"] != um_proj_id and user_info["project"] != args["project_id"]: + state, message = self.send_role_request_and_check_response(ks_member_roleid, user_info["id"], "delete", user_info["project"]) + if not state: + self.logger.error("KS error removing project: {0}".format(message)) + return False, "Keystone error, please try again." + try: + self.keystone.users.update(user_info["id"], email=args["email"], default_project=args["project_id"]) + except exceptions.http.NotFound as ex: + self.logger.error("KS NotFound error: {0}".format(ex)) + return False, "This user does not exist in the keystone!" + except Exception as ex: + self.logger.error("KS general error: {0}".format(ex)) + return False, "{0}".format(ex) + return True, "Updated!" diff --git a/src/access_management/rest-plugin/users_passwords.py b/src/access_management/rest-plugin/users_passwords.py new file mode 100644 index 0000000..9fa7c4a --- /dev/null +++ b/src/access_management/rest-plugin/users_passwords.py @@ -0,0 +1,159 @@ +# Copyright 2019 Nokia + +# 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 json +import crypt +import time +from am_api_base import * +from keystoneauth1 import exceptions +from cmframework.apis import cmclient + + +class UsersPasswords(AMApiBase): + + """ + User reset password operations + + .. :quickref: User passwords;User reset password operations + + .. http:post:: /am/v1/users/passwords + + **Start User reset password** + + **Example request**: + + .. sourcecode:: http + + POST am/v1/users/passwords HTTP/1.1 + Host: haproxyvip:61200 + Accept: application/json + { + "user": or + "npassword: "Passwd_1" + } + + :> json string user: The user's id or name. + :> json string npassword: The user's new password + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + { + "code": 0, + "description": "Users password reset success." + } + + :> json int code: the status code + :> json string description: the error description, present if code is non zero + """ + + endpoints = ['users/passwords'] + parser_arguments = ['user', + 'npassword'] + + def post(self): + self.logger.info("Received a reset password request!") + args = self.parse_args() + + passstate = self.passwd_validator(args["npassword"]) + if passstate is not None: + self.logger.error(passstate) + return AMApiBase.embed_data({}, 1, passstate) + + state, user_info = self.get_uuid_and_name(args["user"]) + if state: + status, message = self._reset_pass(user_info, args['npassword']) + + if status: + self.logger.info("Password reset successfully!") + return AMApiBase.embed_data({}, 0, "Password reset successfully!") + else: + self.logger.error("Internal error: {0}".format(message)) + return AMApiBase.embed_data({}, 1, message) + else: + self.logger.error(user_info) + return AMApiBase.embed_data({}, 1, user_info) + + def _reset_pass(self, user_info, passwd): + try: + reset = self.keystone.users.update(user_info["id"], password=passwd) + except exceptions.http.NotFound as ex: + self.logger.error("{0}".format(ex)) + return False, "This user does not exist in the keystone!" + except Exception as ex: + self.logger.error("{0}".format(ex)) + return False, "{0}".format(ex) + + self.logger.info(reset) + passwd_hash = crypt.crypt(passwd, crypt.mksalt(crypt.METHOD_SHA512)) + + state_open, message_open = self._open_db() + if state_open: + try: + roles = self.db.get_user_roles(user_info["id"]) + for role in roles: + if self.db.is_chroot_role(role): + # if the user has a chroot account, change the pwd of that also + for x in range(3): + self.linux_chroot_pass_handling(user_info["name"], "Chroot", "cloud.chroot", passwd_hash) + time.sleep(5) + if self.check_chroot_linux_pass_state(user_info["name"], "cloud.chroot", passwd_hash): + return True, "Success" + return False, "The user handler is busy, please try again." + if role == "linux_user": + # if the user has a Linux user account, change the pwd of that also + for x in range(3): + self.linux_chroot_pass_handling(user_info["name"], "Linux", "cloud.linuxuser", passwd_hash) + time.sleep(5) + if self.check_chroot_linux_pass_state(user_info["name"], "cloud.linuxuser", passwd_hash): + return True, "Success" + return False, "The user handler is busy, please try again." + except Exception as ex: + self.logger.error("Internal error: {0}".format(ex)) + return AMApiBase.embed_data({}, 1, "Internal error: {0}".format(ex)) + finally: + state_close, message_close = self._close_db() + if not state_close: + self._close_db() + return True, reset + else: + return False, message_open + + def linux_chroot_pass_handling(self, username, user_type, list_name, passwd): + cmc = cmclient.CMClient() + user_list = cmc.get_property(list_name) + user_list = json.loads(user_list) + self.logger.debug("{0} user list before the change: {1}".format(user_type, json.dumps(user_list))) + if user_list is not None: + self.logger.debug("The {0} user list exist!".format(user_type)) + for val in user_list: + if val["name"] == username: + val["password"] = passwd + break + self.logger.debug("{0} user list after the change: {1}".format(user_type, json.dumps(user_list))) + cmc.set_property(list_name, json.dumps(user_list)) + + def check_chroot_linux_pass_state(self, username, list_name, password): + cmc = cmclient.CMClient() + user_list = cmc.get_property(list_name) + user_list = json.loads(user_list) + self.logger.debug("Start the user list check") + for val in user_list: + if val["name"] == username and val["password"] == password: + self.logger.debug("{0} user's password changed!".format(username)) + return True + self.logger.debug("{0} user's password is not changed!".format(username)) + return False diff --git a/src/access_management/rest-plugin/users_roles.py b/src/access_management/rest-plugin/users_roles.py new file mode 100644 index 0000000..9ab641a --- /dev/null +++ b/src/access_management/rest-plugin/users_roles.py @@ -0,0 +1,334 @@ +# Copyright 2019 Nokia + +# 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 time +import access_management.db.amdb as amdb +from am_api_base import * +from cmframework.apis import cmclient + + +class UsersRoles(AMApiBase): + + """ + User add role operations + + .. :quickref: User roles;User add role operations + + .. http:post:: /am/v1/users/roles + + **Start User add role** + + **Example request**: + + .. sourcecode:: http + + POST am/v1/users/roles HTTP/1.1 + Host: haproxyvip:61200 + Accept: application/json + { + "user": or + "role_name": test_role + } + + :> json string user: The user's id or name. + :> json string role_name: The user's new role. + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + { + "code": 0, + "description": "Role add to user." + } + + :> json int code: the status code + :> json string description: the error description, present if code is non zero + + User remove role operations + + .. :quickref: User roles;User remove role operations + + .. http:delete:: /am/v1/users/roles + + **Start User remove role** + + **Example request**: + + .. sourcecode:: http + + DELETE am/v1/users/roles HTTP/1.1 + Host: haproxyvip:61200 + Accept: application/json + { + "user": or + "role_name": test_role + } + + :> json string user: The user's id or name. + :> json string role_name: Remove this role from the user. + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + { + "code": 0, + "description": "Role removed from user." + } + + :> json int code: the status code + :> json string description: the error description, present if code is non zero + """ + + endpoints = ['users/roles'] + parser_arguments = ['user', + 'role_name'] + + def post(self): + self.logger.info("Received a user add role request!") + args = self.parse_args() + + if args["role_name"] is None: + self.logger.error("Role name parameter is missing!") + return AMApiBase.embed_data({}, 1, "Role name parameter is missing!") + + state, user_info = self.get_uuid_and_name(args["user"]) + if state: + username, def_project = self.get_user_from_uuid(user_info["id"]) + state, message = self._add_role(args['role_name'], def_project, user_info) + + if state: + self.logger.info("The {0} role is added to the {1} user!".format(args["role_name"], user_info["name"])) + return AMApiBase.embed_data({}, 0, "Role add to user.") + else: + self.logger.error("The {0} role addition to the {1} user failed: {2}".format(args["role_name"], user_info["name"], message)) + return AMApiBase.construct_error_response(1, message) + else: + self.logger.error(user_info) + return AMApiBase.embed_data({}, 1, user_info) + + def delete(self): + self.logger.info("Received a user remove role request!") + args = self.parse_args() + + if args["role_name"] is None: + self.logger.error("Role name parameter is missing!") + return AMApiBase.embed_data({}, 1, "Role name parameter is missing!") + + state, user_info = self.get_uuid_and_name(args["user"]) + if state: + token_owner = self.get_uuid_from_token() + if user_info["id"] == token_owner and args["role_name"] == defaults.INF_ADMIN_ROLE_NAME: + self.logger.error("The {0} user tried to removed own ".format(user_info["name"])+defaults.INF_ADMIN_ROLE_NAME+" role!") + return AMApiBase.embed_data({}, 1, "You cannot remove own "+defaults.INF_ADMIN_ROLE_NAME+" role!") + + username, def_project = self.get_user_from_uuid(user_info["id"]) + state, message = self._remove_role(args["role_name"], def_project, user_info) + + if state: + self.logger.info("The {0} role removed from the {1} user!".format(args["role_name"], user_info["name"])) + return AMApiBase.embed_data({}, 0, "Role removed from user.") + else: + self.logger.error("Removal of {0} role from {1} user failed: {2}".format(args["role_name"], user_info["name"], message)) + return AMApiBase.construct_error_response(1, message) + else: + self.logger.error(user_info) + return AMApiBase.embed_data({}, 1, user_info) + + def _remove_role(self, role_name, project, user_info): + state_open, message_open = self._open_db() + if state_open: + need_admin_role = True + try: + roles = self.db.get_user_roles(user_info["id"]) + except NotExist: + return False, 'User {0} does not exist.'.format(user_info["name"]) + except Exception as ex: + return False, 'Error retrieving roles for user {0}: {1}'.format(user_info["name"], ex) + if (role_name == defaults.INF_ADMIN_ROLE_NAME and defaults.OS_ADMIN_ROLE_NAME in roles) or (role_name == defaults.OS_ADMIN_ROLE_NAME and defaults.INF_ADMIN_ROLE_NAME in roles): + need_admin_role = False + state, message = self.modify_role_in_keystone(role_name, user_info["id"], "delete", project, need_admin_role) + if not state: + return state, message + + try: +# self.db.connect() + # remove chroot user only if the role is chroot role + self.logger.debug("Check the chroot role, when removing a role!") + if self.db.is_chroot_role(role_name): + self.logger.debug("This is a chroot role!") + for x in range(3): + self.remove_chroot_linux_role_handling(user_info["name"], "Chroot", "cloud.chroot") + time.sleep(2) + if self.check_chroot_linux_state(user_info["name"], "cloud.chroot", "absent"): + self.db.delete_user_role(user_info["id"], role_name) + return True, "Success" + + self.logger.error("The {0} user cannot remove {1} role, because the cm framework set_property's function failed.".format(user_info["name"], role_name)) + return False, "The chroot user is not removed. Please try again!" + + if role_name == "linux_user": + self.logger.debug("This is a linux_user role!") + for x in range(3): + self.remove_chroot_linux_role_handling(user_info["name"], "Linux", "cloud.linuxuser") + time.sleep(2) + if self.check_chroot_linux_state(user_info["name"], "cloud.linuxuser", "absent"): + self.db.delete_user_role(user_info["id"], role_name) + return True, "Success" + + self.logger.error("The {0} user cannot remove {1} role, because the cm framework set_property's function failed.".format(user_info["name"], role_name)) + return False, "The linux user is not removed. Please try again!" + + self.db.delete_user_role(user_info["id"], role_name) + except amdb.NotAllowedOperation: + return False, 'Service role cannot be deleted: {0}'.format(user_info["name"]) + except amdb.NotExist: + return False, 'User {0} has no role {1}.'.format(user_info["name"], role_name) + except amdb.AlreadyExist: + return False, 'Role for user already exists in table: {0}:{1}'.format(user_info["name"], role_name) + except Exception as ex: + return False, ex + finally: + state_close, message_close = self._close_db() + if not state_close: + self._close_db() + return True, "Success" + else: + return False, message_open + + def _add_role(self, role_name, project, user_info): + state, message = self.modify_role_in_keystone(role_name, user_info["id"], "put", project) + if not state: + return state, message + + state, message = self.add_role_db_functions(role_name, user_info) + return state, message + + def add_role_db_functions(self, role_name, user_info): + state_open, message_open = self._open_db() + if state_open: + try: + roles = self.db.get_user_roles(user_info["id"]) + self.db.add_user_role(user_info["id"], role_name) + + # create chroot user only if the role is chroot role + self.logger.debug("Check the chroot role, when adding a role!") + if self.db.is_chroot_role(role_name): + self.logger.debug("This is a chroot role!") + + if "linux_user" in roles: + self.logger.error("The {0} user cannot get {1} chroot role, because this user has a linux_user role".format(user_info["name"], role_name)) + self.db.delete_user_role(user_info["id"], role_name) + return False, "The {0} user cannot get {1} chroot role, because this user has a linux_user role".format(user_info["name"], role_name) + + for x in range(3): + self.add_chroot_linux_role_handling(user_info["id"], "Chroot", "cloud.chroot", role_name) + time.sleep(2) + if self.check_chroot_linux_state(user_info["name"], "cloud.chroot", "present"): + return True, "Success" + + self.db.delete_user_role(user_info["id"], role_name) + self.logger.error("The {0} user cannot get {1} role, because the cm framework set_property's function failed.".format(user_info["name"], role_name)) + return False, "The chroot user is not created. Please try again!" + + # create linux user only if the role is linux_user role + if role_name == "linux_user": + self.logger.debug("This is a linux_user role!") + have_a_chroot = False + self.logger.debug("role list: {0}".format(roles)) + for role in roles: + if self.db.is_chroot_role(role): + have_a_chroot = True + + if have_a_chroot: + self.logger.error("The {0} user cannot get {1} role, because this user has a chroot role".format(user_info["name"], role_name)) + self.db.delete_user_role(user_info["id"], role_name) + return False, "The {0} user cannot get {1} role, because this user has a chroot role".format(user_info["name"], role_name) + + for x in range(3): + self.add_chroot_linux_role_handling(user_info["id"], "Linux", "cloud.linuxuser", None) + time.sleep(2) + if self.check_chroot_linux_state(user_info["name"], "cloud.linuxuser", "present"): + return True, "Success" + + self.db.delete_user_role(user_info["id"], role_name) + self.logger.error("The {0} user cannot get {1} role, because the cm framework set_property's function failed.".format(user_info["name"], role_name)) + return False, "The linux user is not created. Please try again!" + + except amdb.NotExist: + return False, 'The user {} or role {} not exist.'.format(user_info["name"], role_name) + except amdb.AlreadyExist: + return False, 'Role for user already exists in table: {0}:{1}'.format(user_info["name"], role_name) + except Exception as ex: + return False, ex + finally: + state_close, message_close = self._close_db() + if not state_close: + self._close_db() + return True, "Success" + else: + return False, message_open + + def add_chroot_linux_role_handling(self, user_id, user_type, list_name, group): + cmc = cmclient.CMClient() + user_list = cmc.get_property(list_name) + if user_list is None: + cmc.set_property(list_name, json.dumps([])) + user_list = cmc.get_property(list_name) + user_list = json.loads(user_list) + self.logger.debug("{0} user list before the change: {1}".format(user_type, json.dumps(user_list))) + add = True + self.logger.debug("The {0} user list exists!".format(user_type)) + username, def_project = self.get_user_from_uuid(user_id) + self.logger.debug("Username: {0}".format(username)) + for element in user_list: + if element["name"] == username: + if element["state"] == "present": + self.logger.error("The {0} user has an active {1} chroot role".format(username, element["group"])) + self.db.delete_user_role(user_id, group) + return False, "The {0} users have an active {1} chroot role".format(username, element["group"]) + else: + self.logger.debug("The {0} user has an active linux_user role".format(username)) + if group is not None: + element["group"] = group + element["state"] = "present" + element["remove"] = "no" + add = False + if add: + new_user = {"name": username, "password": "", "state": "present", "remove": "no", "lock_state": "-u", "public_key": ""} + if group is not None: + new_user["group"]= group + user_list.append(new_user) + self.logger.debug("{0} user list after the change: {1}".format(user_type, json.dumps(user_list))) + cmc.set_property(list_name, json.dumps(user_list)) + + def remove_chroot_linux_role_handling(self, username, user_type, list_name): + cmc = cmclient.CMClient() + user_list = cmc.get_property(list_name) + user_list = json.loads(user_list) + self.logger.debug("{0} user list before the change: {1}".format(user_type, json.dumps(user_list))) + if user_list is not None: + self.logger.debug("The {0} user list exists!".format(user_type)) + for val in user_list: + if val["name"] == username: + val["public_key"] = "" + val["state"] = "absent" + val["remove"] = "yes" + val["password"] = "" + break + self.logger.debug("{0} user list after the change: {1}".format(user_type, json.dumps(user_list))) + cmc.set_property(list_name, json.dumps(user_list)) diff --git a/src/setup.py b/src/setup.py new file mode 100644 index 0000000..3c34fdc --- /dev/null +++ b/src/setup.py @@ -0,0 +1,62 @@ +# Copyright 2019 Nokia + +# 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 setuptools import setup, find_packages +setup( + name='access_management', + version='1.0', + license='Commercial', + author='Gabor Illes', + author_email='gabor.illes@nokia.com', + platforms=['Any'], + scripts=[], + provides=[], + namespace_packages=['access_management'], + packages=find_packages(), + include_package_data=True, + description='Access Management for Akraino REC blueprint', + install_requires=['flask', 'flask-restful', 'hostcli'], + entry_points={ + 'console_scripts': [ + 'auth-server = access_management.backend.authserver:main', + ], + 'hostcli.commands': [ + 'user create = access_management.cli.cli:CreateNewUser', + 'user delete = access_management.cli.cli:DeleteUsers', + 'user list = access_management.cli.cli:ListUsers', + 'user set password = access_management.cli.cli:ChangeUserPassword', + 'user reset password = access_management.cli.cli:ResetUserPassword', + 'user set parameter = access_management.cli.cli:SetUserParameters', + 'user show = access_management.cli.cli:ShowUserDetails', + 'user showme = access_management.cli.cli:ShowUserOwnDetails', + 'user add role = access_management.cli.cli:AddRoleForUser', + 'user remove role = access_management.cli.cli:RemoveRoleFromUser', + 'user lock = access_management.cli.cli:LockUser', + 'user unlock = access_management.cli.cli:UnlockUser', + 'user add key = access_management.cli.cli:AddKey', + 'user remove key = access_management.cli.cli:RemoveKey', + 'role create = access_management.cli.cli:CreateNewRole', + 'role modify = access_management.cli.cli:ModifyRole', + 'role delete = access_management.cli.cli:DeleteRole', + 'role list all = access_management.cli.cli:ListRoles', + 'role show = access_management.cli.cli:ShowRoleDetails', + 'role list users = access_management.cli.cli:ListUsersOfRole', + 'role add permission = access_management.cli.cli:AddPermissionToRole', + 'role remove permission = access_management.cli.cli:RemovePermissionFromRole', + 'permission list = access_management.cli.cli:ListPermissions', + ], + }, + zip_safe=False, + ) diff --git a/systemd/auth-server.service b/systemd/auth-server.service new file mode 100644 index 0000000..2a58f08 --- /dev/null +++ b/systemd/auth-server.service @@ -0,0 +1,26 @@ +# Copyright 2019 Nokia + +# 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. + +[Unit] +Description=AM Backend server +DefaultDependencies=no + +[Service] +Restart=on-failure +ExecStart=/usr/local/bin/auth-server +User=access-manager +RestartSec=5 + +[Install] +WantedBy=multi-user.target -- 2.16.6