Added seed code for access-management. 35/635/1
authorIlles Gabor <gabor.illes@nokia.com>
Wed, 8 May 2019 14:29:13 +0000 (16:29 +0200)
committergabor.illes <gabor.illes@nokia.com>
Wed, 8 May 2019 14:32:57 +0000 (16:32 +0200)
Added seed code for access-management.

Change-Id: I4c38a5edc8166fb8babf38458d255f0115b438ab
Signed-off-by: gabor.illes <gabor.illes@nokia.com>
42 files changed:
.gitreview [new file with mode: 0644]
LICENSE [new file with mode: 0644]
access-management.spec [new file with mode: 0644]
config-encoder-macros.spec [new file with mode: 0644]
secrets/am-secrets.yaml [new file with mode: 0644]
src/__init__.py [new file with mode: 0644]
src/access_management/__init__.py [new file with mode: 0644]
src/access_management/backend/__init__.py [new file with mode: 0644]
src/access_management/backend/am_auth.py [new file with mode: 0644]
src/access_management/backend/ambackend.py [new file with mode: 0644]
src/access_management/backend/authsender.py [new file with mode: 0644]
src/access_management/backend/authserver.py [new file with mode: 0644]
src/access_management/backend/restlogger.py [new file with mode: 0644]
src/access_management/cli/__init__.py [new file with mode: 0644]
src/access_management/cli/cli.py [new file with mode: 0644]
src/access_management/config/__init__.py [new file with mode: 0644]
src/access_management/config/amconfigparser.py [new file with mode: 0644]
src/access_management/config/defaults.py [new file with mode: 0644]
src/access_management/cryptohelper/__init__.py [new file with mode: 0644]
src/access_management/cryptohelper/decryptaaafiles.py [new file with mode: 0644]
src/access_management/cryptohelper/encryptaaafiles.py [new file with mode: 0644]
src/access_management/db/__init__.py [new file with mode: 0644]
src/access_management/db/amdb.py [new file with mode: 0644]
src/access_management/rest-plugin/__init__.py [new file with mode: 0644]
src/access_management/rest-plugin/am.ini [new file with mode: 0644]
src/access_management/rest-plugin/am_api_base.py [new file with mode: 0644]
src/access_management/rest-plugin/permissions.py [new file with mode: 0644]
src/access_management/rest-plugin/roles.py [new file with mode: 0644]
src/access_management/rest-plugin/roles_details.py [new file with mode: 0644]
src/access_management/rest-plugin/roles_permissions.py [new file with mode: 0644]
src/access_management/rest-plugin/roles_users.py [new file with mode: 0644]
src/access_management/rest-plugin/users.py [new file with mode: 0644]
src/access_management/rest-plugin/users_details.py [new file with mode: 0644]
src/access_management/rest-plugin/users_keys.py [new file with mode: 0644]
src/access_management/rest-plugin/users_locks.py [new file with mode: 0644]
src/access_management/rest-plugin/users_owndetails.py [new file with mode: 0644]
src/access_management/rest-plugin/users_ownpasswords.py [new file with mode: 0644]
src/access_management/rest-plugin/users_parameters.py [new file with mode: 0644]
src/access_management/rest-plugin/users_passwords.py [new file with mode: 0644]
src/access_management/rest-plugin/users_roles.py [new file with mode: 0644]
src/setup.py [new file with mode: 0644]
systemd/auth-server.service [new file with mode: 0644]

diff --git a/.gitreview b/.gitreview
new file mode 100644 (file)
index 0000000..b7d86ad
--- /dev/null
@@ -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 (file)
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 (file)
index 0000000..20f813e
--- /dev/null
@@ -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 (file)
index 0000000..83c5d31
--- /dev/null
@@ -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 (file)
index 0000000..65980b8
--- /dev/null
@@ -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 (file)
index 0000000..287b513
--- /dev/null
@@ -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 (file)
index 0000000..287b513
--- /dev/null
@@ -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 (file)
index 0000000..78c5878
--- /dev/null
@@ -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 (file)
index 0000000..7b3f94d
--- /dev/null
@@ -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 (file)
index 0000000..90e1402
--- /dev/null
@@ -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 (file)
index 0000000..57a332b
--- /dev/null
@@ -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 (file)
index 0000000..e21fb17
--- /dev/null
@@ -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 (file)
index 0000000..2d38140
--- /dev/null
@@ -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 (file)
index 0000000..78c5878
--- /dev/null
@@ -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 (file)
index 0000000..15b27d9
--- /dev/null
@@ -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 <key>[:<asc|desc>]. 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 (file)
index 0000000..78c5878
--- /dev/null
@@ -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 (file)
index 0000000..e33958b
--- /dev/null
@@ -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 (file)
index 0000000..39e43d6
--- /dev/null
@@ -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 (file)
index 0000000..78c5878
--- /dev/null
@@ -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 (file)
index 0000000..4acf203
--- /dev/null
@@ -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 (file)
index 0000000..873907f
--- /dev/null
@@ -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 (file)
index 0000000..78c5878
--- /dev/null
@@ -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 (file)
index 0000000..46f6d2f
--- /dev/null
@@ -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 (file)
index 0000000..78c5878
--- /dev/null
@@ -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 (file)
index 0000000..a7c115d
--- /dev/null
@@ -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 (file)
index 0000000..b1b01d0
--- /dev/null
@@ -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 (file)
index 0000000..ecb1f6c
--- /dev/null
@@ -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 (file)
index 0000000..d1bea48
--- /dev/null
@@ -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 (file)
index 0000000..c12c738
--- /dev/null
@@ -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 (file)
index 0000000..c5a64d5
--- /dev/null
@@ -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 (file)
index 0000000..87176d0
--- /dev/null
@@ -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 (file)
index 0000000..ec708c8
--- /dev/null
@@ -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": <uuid>
+            }
+        }
+
+    :> 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": <uuid> or <username>
+        }
+
+    :> 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 (file)
index 0000000..c3df794
--- /dev/null
@@ -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": <uuid> or <username>
+        }
+
+    :> 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 (file)
index 0000000..24ee243
--- /dev/null
@@ -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": <uuid> or <username>
+            "key": <user 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": <uuid> or <username>
+        }
+
+    :> 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 (file)
index 0000000..081c834
--- /dev/null
@@ -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": <uuid> or <username>
+        }
+
+    :> 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": <uuid> or <username>
+        }
+
+    :> 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 (file)
index 0000000..30467da
--- /dev/null
@@ -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 (file)
index 0000000..1dbd2c2
--- /dev/null
@@ -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 (file)
index 0000000..37b2d6c
--- /dev/null
@@ -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": <uuid> or <username>
+            "project_id: <project_id>
+            "email": <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 (file)
index 0000000..9fa7c4a
--- /dev/null
@@ -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": <uuid> or <username>
+            "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 (file)
index 0000000..9ab641a
--- /dev/null
@@ -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": <uuid> or <username>
+            "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": <uuid> or <username>
+            "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 (file)
index 0000000..3c34fdc
--- /dev/null
@@ -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 (file)
index 0000000..2a58f08
--- /dev/null
@@ -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