Added seed code for access-management.
Change-Id: I4c38a5edc8166fb8babf38458d255f0115b438ab
Signed-off-by: gabor.illes <gabor.illes@nokia.com>
--- /dev/null
+[gerrit]
+host=gerrit.att-akraino.org
+port=29418
+project=rec/access-management
+defaultremote=origin
--- /dev/null
+
+ 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.
--- /dev/null
+# 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}
--- /dev/null
+# 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}
--- /dev/null
+# 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:
--- /dev/null
+# 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__)
--- /dev/null
+# 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__)
--- /dev/null
+# 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.
+
--- /dev/null
+# 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
--- /dev/null
+#!/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
--- /dev/null
+# 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()
--- /dev/null
+# 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)
--- /dev/null
+# 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()
--- /dev/null
+# 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.
+
--- /dev/null
+# 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.'
--- /dev/null
+# 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.
+
--- /dev/null
+#!/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
--- /dev/null
+# 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"
--- /dev/null
+# 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.
+
--- /dev/null
+# 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)
--- /dev/null
+# 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)
--- /dev/null
+# 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.
+
--- /dev/null
+#!/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
--- /dev/null
+# 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.
+
--- /dev/null
+# 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
--- /dev/null
+# 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)}
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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!"
--- /dev/null
+# 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
--- /dev/null
+# 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))
--- /dev/null
+# 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
--- /dev/null
+# 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))
--- /dev/null
+# 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))
--- /dev/null
+# 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
--- /dev/null
+# 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))
--- /dev/null
+# 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!"
--- /dev/null
+# 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
--- /dev/null
+# 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))
--- /dev/null
+# 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,
+ )
--- /dev/null
+# 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