blob: 05f803af29dc33b1035aa32f0d61214efb2ef860 [file] [log] [blame]
Eduardo Sousa819d34c2018-07-31 01:20:02 +01001# -*- coding: utf-8 -*-
2
Eduardo Sousad795f872019-02-05 16:05:53 +00003# Copyright 2018 Whitestack, LLC
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
16#
17# For those usages not covered by the Apache License, Version 2.0 please
18# contact: esousa@whitestack.com or glavado@whitestack.com
19##
20
Eduardo Sousa819d34c2018-07-31 01:20:02 +010021"""
22AuthconnKeystone implements implements the connector for
23Openstack Keystone and leverages the RBAC model, to bring
24it for OSM.
25"""
Eduardo Sousa44603902019-06-04 08:10:32 +010026
Eduardo Sousa819d34c2018-07-31 01:20:02 +010027
tierno23acf402019-08-28 13:36:34 +000028__author__ = "Eduardo Sousa <esousa@whitestack.com>, " \
29 "Pedro de la Cruz Ramos <pdelacruzramos@altran.com>"
Eduardo Sousa819d34c2018-07-31 01:20:02 +010030__date__ = "$27-jul-2018 23:59:59$"
31
tierno23acf402019-08-28 13:36:34 +000032from osm_nbi.authconn import Authconn, AuthException, AuthconnOperationException, AuthconnNotFoundException, \
tierno1f029d82019-06-13 22:37:04 +000033 AuthconnConflictException
Eduardo Sousa819d34c2018-07-31 01:20:02 +010034
35import logging
Eduardo Sousa29933fc2018-11-14 06:36:35 +000036import requests
Eduardo Sousa44603902019-06-04 08:10:32 +010037import time
Eduardo Sousa819d34c2018-07-31 01:20:02 +010038from keystoneauth1 import session
39from keystoneauth1.identity import v3
40from keystoneauth1.exceptions.base import ClientException
Eduardo Sousa29933fc2018-11-14 06:36:35 +000041from keystoneauth1.exceptions.http import Conflict
Eduardo Sousa819d34c2018-07-31 01:20:02 +010042from keystoneclient.v3 import client
43from http import HTTPStatus
K Sai Kiran990ac462020-05-20 12:25:12 +053044from osm_nbi.validation import is_valid_uuid, validate_input, http_schema
Eduardo Sousa819d34c2018-07-31 01:20:02 +010045
46
47class AuthconnKeystone(Authconn):
tierno9e87a7f2020-03-23 09:24:10 +000048 def __init__(self, config, db, role_permissions):
49 Authconn.__init__(self, config, db, role_permissions)
Eduardo Sousa819d34c2018-07-31 01:20:02 +010050
51 self.logger = logging.getLogger("nbi.authenticator.keystone")
tiernoad6d5332020-02-19 14:29:49 +000052 self.domains_id2name = {}
53 self.domains_name2id = {}
Eduardo Sousa819d34c2018-07-31 01:20:02 +010054
K Sai Kiran990ac462020-05-20 12:25:12 +053055 self.auth_url = config.get("auth_url")
56 if config.get("auth_url"):
57 validate_input(self.auth_url, http_schema)
58 else:
59 self.auth_url = "http://{0}:{1}/v3".format(config.get("auth_host", "keystone"),
60 config.get("auth_port", "5000"))
tierno6486f742020-02-13 16:30:14 +000061 self.user_domain_name_list = config.get("user_domain_name", "default")
62 self.user_domain_name_list = self.user_domain_name_list.split(",")
tiernoad6d5332020-02-19 14:29:49 +000063 # read only domain list
64 self.user_domain_ro_list = [x[:-3] for x in self.user_domain_name_list if x.endswith(":ro")]
65 # remove the ":ro"
66 self.user_domain_name_list = [x if not x.endswith(":ro") else x[:-3] for x in self.user_domain_name_list]
67
Eduardo Sousa819d34c2018-07-31 01:20:02 +010068 self.admin_project = config.get("service_project", "service")
69 self.admin_username = config.get("service_username", "nbi")
70 self.admin_password = config.get("service_password", "nbi")
tierno6486f742020-02-13 16:30:14 +000071 self.project_domain_name_list = config.get("project_domain_name", "default")
72 self.project_domain_name_list = self.project_domain_name_list.split(",")
73 if len(self.user_domain_name_list) != len(self.project_domain_name_list):
74 raise ValueError("Invalid configuration parameter fo authenticate. 'project_domain_name' and "
75 "'user_domain_name' must be a comma-separated list with the same size. Revise "
76 "configuration or/and 'OSMNBI_AUTHENTICATION_PROJECT_DOMAIN_NAME', "
77 "'OSMNBI_AUTHENTICATION_USER_DOMAIN_NAME' Variables")
Eduardo Sousa819d34c2018-07-31 01:20:02 +010078
Eduardo Sousa29933fc2018-11-14 06:36:35 +000079 # Waiting for Keystone to be up
80 available = None
81 counter = 300
82 while available is None:
83 time.sleep(1)
84 try:
85 result = requests.get(self.auth_url)
86 available = True if result.status_code == 200 else None
87 except Exception:
88 counter -= 1
89 if counter == 0:
90 raise AuthException("Keystone not available after 300s timeout")
91
tierno6486f742020-02-13 16:30:14 +000092 self.auth = v3.Password(user_domain_name=self.user_domain_name_list[0],
Eduardo Sousa819d34c2018-07-31 01:20:02 +010093 username=self.admin_username,
94 password=self.admin_password,
tierno6486f742020-02-13 16:30:14 +000095 project_domain_name=self.project_domain_name_list[0],
Eduardo Sousa819d34c2018-07-31 01:20:02 +010096 project_name=self.admin_project,
97 auth_url=self.auth_url)
98 self.sess = session.Session(auth=self.auth)
K Sai Kiran990ac462020-05-20 12:25:12 +053099 self.keystone = client.Client(session=self.sess, endpoint_override=self.auth_url)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100100
tierno6486f742020-02-13 16:30:14 +0000101 def authenticate(self, credentials, token_info=None):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100102 """
tierno701018c2019-06-25 11:13:14 +0000103 Authenticate a user using username/password or token_info, plus project
tierno6486f742020-02-13 16:30:14 +0000104 :param credentials: dictionary that contains:
105 username: name, id or None
106 password: password or None
107 project_id: name, id, or None. If None first found project will be used to get an scope token
108 project_domain_name: (Optional) To use a concrete domain for the project
109 user_domain_name: (Optional) To use a concrete domain for the project
110 other items are allowed and ignored
tierno701018c2019-06-25 11:13:14 +0000111 :param token_info: previous token_info to obtain authorization
tierno38dcfeb2019-06-10 16:44:00 +0000112 :return: the scoped token info or raises an exception. The token is a dictionary with:
113 _id: token string id,
114 username: username,
115 project_id: scoped_token project_id,
116 project_name: scoped_token project_name,
117 expires: epoch time when it expires,
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100118
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100119 """
tierno6486f742020-02-13 16:30:14 +0000120 username = None
121 user_id = None
122 project_id = None
123 project_name = None
124 if credentials.get("project_domain_name"):
125 project_domain_name_list = (credentials["project_domain_name"], )
126 else:
127 project_domain_name_list = self.project_domain_name_list
128 if credentials.get("user_domain_name"):
129 user_domain_name_list = (credentials["user_domain_name"], )
130 else:
131 user_domain_name_list = self.user_domain_name_list
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100132
tierno6486f742020-02-13 16:30:14 +0000133 for index, project_domain_name in enumerate(project_domain_name_list):
134 user_domain_name = user_domain_name_list[index]
135 try:
136 if credentials.get("username"):
137 if is_valid_uuid(credentials["username"]):
138 user_id = credentials["username"]
139 else:
140 username = credentials["username"]
141
142 # get an unscoped token firstly
143 unscoped_token = self.keystone.get_raw_token_from_identity_service(
144 auth_url=self.auth_url,
145 user_id=user_id,
146 username=username,
147 password=credentials.get("password"),
148 user_domain_name=user_domain_name,
149 project_domain_name=project_domain_name)
150 elif token_info:
151 unscoped_token = self.keystone.tokens.validate(token=token_info.get("_id"))
tierno38dcfeb2019-06-10 16:44:00 +0000152 else:
tierno6486f742020-02-13 16:30:14 +0000153 raise AuthException("Provide credentials: username/password or Authorization Bearer token",
154 http_code=HTTPStatus.UNAUTHORIZED)
tierno38dcfeb2019-06-10 16:44:00 +0000155
tierno6486f742020-02-13 16:30:14 +0000156 if not credentials.get("project_id"):
157 # get first project for the user
158 project_list = self.keystone.projects.list(user=unscoped_token["user"]["id"])
159 if not project_list:
160 raise AuthException("The user {} has not any project and cannot be used for authentication".
161 format(credentials.get("username")), http_code=HTTPStatus.UNAUTHORIZED)
162 project_id = project_list[0].id
163 else:
164 if is_valid_uuid(credentials["project_id"]):
165 project_id = credentials["project_id"]
166 else:
167 project_name = credentials["project_id"]
168
169 scoped_token = self.keystone.get_raw_token_from_identity_service(
tierno38dcfeb2019-06-10 16:44:00 +0000170 auth_url=self.auth_url,
tierno6486f742020-02-13 16:30:14 +0000171 project_name=project_name,
172 project_id=project_id,
173 user_domain_name=user_domain_name,
174 project_domain_name=project_domain_name,
175 token=unscoped_token["auth_token"])
tierno38dcfeb2019-06-10 16:44:00 +0000176
tierno6486f742020-02-13 16:30:14 +0000177 auth_token = {
178 "_id": scoped_token.auth_token,
179 "id": scoped_token.auth_token,
180 "user_id": scoped_token.user_id,
181 "username": scoped_token.username,
182 "project_id": scoped_token.project_id,
183 "project_name": scoped_token.project_name,
184 "project_domain_name": scoped_token.project_domain_name,
185 "user_domain_name": scoped_token.user_domain_name,
186 "expires": scoped_token.expires.timestamp(),
187 "issued_at": scoped_token.issued.timestamp()
188 }
tierno38dcfeb2019-06-10 16:44:00 +0000189
tierno6486f742020-02-13 16:30:14 +0000190 return auth_token
191 except ClientException as e:
192 if index >= len(user_domain_name_list)-1 or index >= len(project_domain_name_list)-1:
193 # if last try, launch exception
194 # self.logger.exception("Error during user authentication using keystone: {}".format(e))
195 raise AuthException("Error during user authentication using Keystone: {}".format(e),
196 http_code=HTTPStatus.UNAUTHORIZED)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100197
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100198 def validate_token(self, token):
199 """
200 Check if the token is valid.
201
tierno701018c2019-06-25 11:13:14 +0000202 :param token: token id to be validated
tiernoa6bb45d2019-06-14 09:45:39 +0000203 :return: dictionary with information associated with the token:
204 "expires":
205 "_id": token_id,
206 "project_id": project_id,
207 "username": ,
208 "roles": list with dict containing {name, id}
209 If the token is not valid an exception is raised.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100210 """
211 if not token:
212 return
213
214 try:
215 token_info = self.keystone.tokens.validate(token=token)
tiernoa6bb45d2019-06-14 09:45:39 +0000216 ses = {
217 "_id": token_info["auth_token"],
tierno701018c2019-06-25 11:13:14 +0000218 "id": token_info["auth_token"],
tiernoa6bb45d2019-06-14 09:45:39 +0000219 "project_id": token_info["project"]["id"],
220 "project_name": token_info["project"]["name"],
221 "user_id": token_info["user"]["id"],
222 "username": token_info["user"]["name"],
223 "roles": token_info["roles"],
tierno701018c2019-06-25 11:13:14 +0000224 "expires": token_info.expires.timestamp(),
225 "issued_at": token_info.issued.timestamp()
tiernoa6bb45d2019-06-14 09:45:39 +0000226 }
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100227
tiernoa6bb45d2019-06-14 09:45:39 +0000228 return ses
tierno4015b472019-06-10 13:57:29 +0000229 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000230 # self.logger.exception("Error during token validation using keystone: {}".format(e))
tierno4015b472019-06-10 13:57:29 +0000231 raise AuthException("Error during token validation using Keystone: {}".format(e),
232 http_code=HTTPStatus.UNAUTHORIZED)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100233
234 def revoke_token(self, token):
235 """
236 Invalidate a token.
237
238 :param token: token to be revoked
239 """
240 try:
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000241 self.logger.info("Revoking token: " + token)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100242 self.keystone.tokens.revoke_token(token=token)
243
244 return True
tierno4015b472019-06-10 13:57:29 +0000245 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000246 # self.logger.exception("Error during token revocation using keystone: {}".format(e))
tierno4015b472019-06-10 13:57:29 +0000247 raise AuthException("Error during token revocation using Keystone: {}".format(e),
248 http_code=HTTPStatus.UNAUTHORIZED)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100249
tiernoad6d5332020-02-19 14:29:49 +0000250 def _get_domain_id(self, domain_name, fail_if_not_found=True):
251 """
252 Get the domain id from the domain_name
253 :param domain_name: Can be the name or id
254 :param fail_if_not_found: If False it returns None instead of raising an exception if not found
255 :return: str or None/exception if domain is not found
256 """
257 domain_id = self.domains_name2id.get(domain_name)
258 if not domain_id:
259 self._get_domains()
260 domain_id = self.domains_name2id.get(domain_name)
261 if not domain_id and domain_name in self.domains_id2name:
262 # domain_name is already an id
263 return domain_name
264 if not domain_id and fail_if_not_found:
265 raise AuthconnNotFoundException("Domain {} cannot be found".format(domain_name))
266 return domain_id
267
268 def _get_domains(self):
269 """
270 Obtain a dictionary with domain_id to domain_name, stored at self.domains_id2name
271 and from domain_name to domain_id, sored at self.domains_name2id
272 :return: None. Exceptions are ignored
273 """
274 try:
275 domains = self.keystone.domains.list()
276 self.domains_id2name = {x.id: x.name for x in domains}
277 self.domains_name2id = {x.name: x.id for x in domains}
278 except Exception:
279 pass
280
delacruzramo01b15d32019-07-02 14:37:47 +0200281 def create_user(self, user_info):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100282 """
283 Create a user.
284
delacruzramo01b15d32019-07-02 14:37:47 +0200285 :param user_info: full user info.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100286 :raises AuthconnOperationException: if user creation failed.
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100287 :return: returns the id of the user in keystone.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100288 """
289 try:
tiernoad6d5332020-02-19 14:29:49 +0000290
291 if user_info.get("domain_name") and user_info["domain_name"] in self.user_domain_ro_list:
292 raise AuthconnConflictException("Cannot create a user in the read only domain {}".
293 format(user_info["domain_name"]))
294
tierno6486f742020-02-13 16:30:14 +0000295 new_user = self.keystone.users.create(
296 user_info["username"], password=user_info["password"],
tiernoad6d5332020-02-19 14:29:49 +0000297 domain=self._get_domain_id(user_info.get("domain_name", self.user_domain_name_list[0])),
tierno6486f742020-02-13 16:30:14 +0000298 _admin=user_info["_admin"])
delacruzramo01b15d32019-07-02 14:37:47 +0200299 if "project_role_mappings" in user_info.keys():
300 for mapping in user_info["project_role_mappings"]:
tiernoad6d5332020-02-19 14:29:49 +0000301 self.assign_role_to_user(new_user, mapping["project"], mapping["role"])
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100302 return {"username": new_user.name, "_id": new_user.id}
tiernocf042d32019-06-13 09:06:40 +0000303 except Conflict as e:
304 # self.logger.exception("Error during user creation using keystone: {}".format(e))
305 raise AuthconnOperationException(e, http_code=HTTPStatus.CONFLICT)
tierno4015b472019-06-10 13:57:29 +0000306 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000307 # self.logger.exception("Error during user creation using keystone: {}".format(e))
tierno4015b472019-06-10 13:57:29 +0000308 raise AuthconnOperationException("Error during user creation using Keystone: {}".format(e))
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100309
delacruzramo01b15d32019-07-02 14:37:47 +0200310 def update_user(self, user_info):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100311 """
tiernocf042d32019-06-13 09:06:40 +0000312 Change the user name and/or password.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100313
delacruzramo01b15d32019-07-02 14:37:47 +0200314 :param user_info: user info modifications
tiernocf042d32019-06-13 09:06:40 +0000315 :raises AuthconnOperationException: if change failed.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100316 """
317 try:
delacruzramo01b15d32019-07-02 14:37:47 +0200318 user = user_info.get("_id") or user_info.get("username")
tiernoad6d5332020-02-19 14:29:49 +0000319 try:
320 user_obj = self.keystone.users.get(user)
321 except Exception:
322 user_obj = None
323 if not user_obj:
324 for user_domain in self.user_domain_name_list:
325 domain_id = self._get_domain_id(user_domain, fail_if_not_found=False)
326 if not domain_id:
327 continue
328 user_obj_list = self.keystone.users.list(name=user, domain=domain_id)
329 if user_obj_list:
330 user_obj = user_obj_list[0]
331 break
332 else: # user not found
333 raise AuthconnNotFoundException("User '{}' not found".format(user))
334
delacruzramo01b15d32019-07-02 14:37:47 +0200335 user_id = user_obj.id
tiernoad6d5332020-02-19 14:29:49 +0000336 domain_id = user_obj.domain_id
337 domain_name = self.domains_id2name.get(domain_id)
338
339 if domain_name in self.user_domain_ro_list:
340 if user_info.get("password") or user_info.get("username"):
341 raise AuthconnConflictException("Cannot update the user {} belonging to a read only domain {}".
342 format(user, domain_name))
343
344 elif user_info.get("password") or user_info.get("username") \
delacruzramo01b15d32019-07-02 14:37:47 +0200345 or user_info.get("add_project_role_mappings") or user_info.get("remove_project_role_mappings"):
tiernoad6d5332020-02-19 14:29:49 +0000346 # if user_index>0, it is an external domain, that should not be updated
delacruzramo15ec7062019-12-26 10:09:04 +0000347 ctime = user_obj._admin.get("created", 0) if hasattr(user_obj, "_admin") else 0
K Sai Kiran974276d2020-05-27 16:30:10 +0530348 try:
349 self.keystone.users.update(user_id, password=user_info.get("password"),
350 name=user_info.get("username"),
351 _admin={"created": ctime, "modified": time.time()})
352 except Exception as e:
353 if user_info.get("username") or user_info.get("password"):
354 raise AuthconnOperationException("Error during username/password change: {}".format(str(e)))
355 self.logger.error("Error during updating user profile: {}".format(str(e)))
tiernoad6d5332020-02-19 14:29:49 +0000356
delacruzramo01b15d32019-07-02 14:37:47 +0200357 for mapping in user_info.get("remove_project_role_mappings", []):
tiernoad6d5332020-02-19 14:29:49 +0000358 self.remove_role_from_user(user_obj, mapping["project"], mapping["role"])
delacruzramo01b15d32019-07-02 14:37:47 +0200359 for mapping in user_info.get("add_project_role_mappings", []):
tiernoad6d5332020-02-19 14:29:49 +0000360 self.assign_role_to_user(user_obj, mapping["project"], mapping["role"])
tierno4015b472019-06-10 13:57:29 +0000361 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000362 # self.logger.exception("Error during user password/name update using keystone: {}".format(e))
delacruzramo01b15d32019-07-02 14:37:47 +0200363 raise AuthconnOperationException("Error during user update using Keystone: {}".format(e))
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100364
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100365 def delete_user(self, user_id):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100366 """
367 Delete user.
368
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100369 :param user_id: user identifier.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100370 :raises AuthconnOperationException: if user deletion failed.
371 """
372 try:
tiernoad6d5332020-02-19 14:29:49 +0000373 user_obj = self.keystone.users.get(user_id)
374 domain_id = user_obj.domain_id
375 domain_name = self.domains_id2name.get(domain_id)
376 if domain_name in self.user_domain_ro_list:
377 raise AuthconnConflictException("Cannot delete user {} belonging to a read only domain {}".
378 format(user_id, domain_name))
379
tierno38dcfeb2019-06-10 16:44:00 +0000380 result, detail = self.keystone.users.delete(user_id)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100381 if result.status_code != 204:
tierno38dcfeb2019-06-10 16:44:00 +0000382 raise ClientException("error {} {}".format(result.status_code, detail))
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100383 return True
tierno4015b472019-06-10 13:57:29 +0000384 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000385 # self.logger.exception("Error during user deletion using keystone: {}".format(e))
tierno4015b472019-06-10 13:57:29 +0000386 raise AuthconnOperationException("Error during user deletion using Keystone: {}".format(e))
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100387
tiernocf042d32019-06-13 09:06:40 +0000388 def get_user_list(self, filter_q=None):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100389 """
390 Get user list.
391
tiernoad6d5332020-02-19 14:29:49 +0000392 :param filter_q: dictionary to filter user list by one or several
393 _id:
394 name (username is also admitted). If a user id is equal to the filter name, it is also provided
395 domain_id, domain_name
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100396 :return: returns a list of users.
397 """
398 try:
tiernoad6d5332020-02-19 14:29:49 +0000399 self._get_domains()
400 filter_name = filter_domain = None
tiernocf042d32019-06-13 09:06:40 +0000401 if filter_q:
402 filter_name = filter_q.get("name") or filter_q.get("username")
tiernoad6d5332020-02-19 14:29:49 +0000403 if filter_q.get("domain_name"):
404 filter_domain = self._get_domain_id(filter_q["domain_name"], fail_if_not_found=False)
405 # If domain is not found, use the same name to obtain an empty list
406 filter_domain = filter_domain or filter_q["domain_name"]
407 if filter_q.get("domain_id"):
408 filter_domain = filter_q["domain_id"]
409
410 users = self.keystone.users.list(name=filter_name, domain=filter_domain)
411 # get users from user_domain_name_list[1:], because it will not be provided in case of LDAP
412 if filter_domain is None and len(self.user_domain_name_list) > 1:
413 for user_domain in self.user_domain_name_list[1:]:
414 domain_id = self._get_domain_id(user_domain, fail_if_not_found=False)
415 if not domain_id:
416 continue
417 # find if users of this domain are already provided. In this case ignore
418 for u in users:
419 if u.domain_id == domain_id:
420 break
421 else:
422 users += self.keystone.users.list(name=filter_name, domain=domain_id)
423
424 # if filter name matches a user id, provide it also
425 if filter_name:
426 try:
427 user_obj = self.keystone.users.get(filter_name)
428 if user_obj not in users:
429 users.append(user_obj)
430 except Exception:
431 pass
432
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100433 users = [{
434 "username": user.name,
Eduardo Sousa203bad82019-05-23 01:41:18 +0100435 "_id": user.id,
delacruzramo01b15d32019-07-02 14:37:47 +0200436 "id": user.id,
tiernoad6d5332020-02-19 14:29:49 +0000437 "_admin": user.to_dict().get("_admin", {}), # TODO: REVISE
438 "domain_name": self.domains_id2name.get(user.domain_id)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100439 } for user in users if user.name != self.admin_username]
440
tiernocf042d32019-06-13 09:06:40 +0000441 if filter_q and filter_q.get("_id"):
442 users = [user for user in users if filter_q["_id"] == user["_id"]]
Eduardo Sousa2d5a5152019-05-20 15:41:54 +0100443
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100444 for user in users:
tierno1546f2a2019-08-20 15:38:11 +0000445 user["project_role_mappings"] = []
446 user["projects"] = []
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100447 projects = self.keystone.projects.list(user=user["_id"])
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100448 for project in projects:
tierno1546f2a2019-08-20 15:38:11 +0000449 user["projects"].append(project.name)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100450
tierno1546f2a2019-08-20 15:38:11 +0000451 roles = self.keystone.roles.list(user=user["_id"], project=project.id)
452 for role in roles:
453 prm = {
454 "project": project.id,
455 "project_name": project.name,
456 "role_name": role.name,
457 "role": role.id,
458 }
459 user["project_role_mappings"].append(prm)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100460
461 return users
tierno4015b472019-06-10 13:57:29 +0000462 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000463 # self.logger.exception("Error during user listing using keystone: {}".format(e))
tierno4015b472019-06-10 13:57:29 +0000464 raise AuthconnOperationException("Error during user listing using Keystone: {}".format(e))
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100465
tierno1f029d82019-06-13 22:37:04 +0000466 def get_role_list(self, filter_q=None):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100467 """
468 Get role list.
469
tierno1f029d82019-06-13 22:37:04 +0000470 :param filter_q: dictionary to filter role list by _id and/or name.
Eduardo Sousa37de0912019-05-23 02:17:22 +0100471 :return: returns the list of roles.
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100472 """
473 try:
tierno1f029d82019-06-13 22:37:04 +0000474 filter_name = None
475 if filter_q:
476 filter_name = filter_q.get("name")
477 roles_list = self.keystone.roles.list(name=filter_name)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100478
479 roles = [{
480 "name": role.name,
delacruzramo01b15d32019-07-02 14:37:47 +0200481 "_id": role.id,
482 "_admin": role.to_dict().get("_admin", {}),
483 "permissions": role.to_dict().get("permissions", {})
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100484 } for role in roles_list if role.name != "service"]
485
tierno1f029d82019-06-13 22:37:04 +0000486 if filter_q and filter_q.get("_id"):
487 roles = [role for role in roles if filter_q["_id"] == role["_id"]]
488
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100489 return roles
tierno4015b472019-06-10 13:57:29 +0000490 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000491 # self.logger.exception("Error during user role listing using keystone: {}".format(e))
tierno4015b472019-06-10 13:57:29 +0000492 raise AuthException("Error during user role listing using Keystone: {}".format(e),
493 http_code=HTTPStatus.UNAUTHORIZED)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100494
delacruzramo01b15d32019-07-02 14:37:47 +0200495 def create_role(self, role_info):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100496 """
497 Create a role.
498
delacruzramo01b15d32019-07-02 14:37:47 +0200499 :param role_info: full role info.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100500 :raises AuthconnOperationException: if role creation failed.
501 """
502 try:
delacruzramo01b15d32019-07-02 14:37:47 +0200503 result = self.keystone.roles.create(role_info["name"], permissions=role_info.get("permissions"),
504 _admin=role_info.get("_admin"))
tierno1f029d82019-06-13 22:37:04 +0000505 return result.id
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000506 except Conflict as ex:
tierno1f029d82019-06-13 22:37:04 +0000507 raise AuthconnConflictException(str(ex))
tierno4015b472019-06-10 13:57:29 +0000508 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000509 # self.logger.exception("Error during role creation using keystone: {}".format(e))
tierno4015b472019-06-10 13:57:29 +0000510 raise AuthconnOperationException("Error during role creation using Keystone: {}".format(e))
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100511
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100512 def delete_role(self, role_id):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100513 """
514 Delete a role.
515
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100516 :param role_id: role identifier.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100517 :raises AuthconnOperationException: if role deletion failed.
518 """
519 try:
tierno1f029d82019-06-13 22:37:04 +0000520 result, detail = self.keystone.roles.delete(role_id)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100521
522 if result.status_code != 204:
tierno38dcfeb2019-06-10 16:44:00 +0000523 raise ClientException("error {} {}".format(result.status_code, detail))
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100524
525 return True
tierno4015b472019-06-10 13:57:29 +0000526 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000527 # self.logger.exception("Error during role deletion using keystone: {}".format(e))
tierno4015b472019-06-10 13:57:29 +0000528 raise AuthconnOperationException("Error during role deletion using Keystone: {}".format(e))
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100529
delacruzramo01b15d32019-07-02 14:37:47 +0200530 def update_role(self, role_info):
tierno1f029d82019-06-13 22:37:04 +0000531 """
532 Change the name of a role
delacruzramo01b15d32019-07-02 14:37:47 +0200533 :param role_info: full role info
tierno1f029d82019-06-13 22:37:04 +0000534 :return: None
535 """
536 try:
delacruzramo01b15d32019-07-02 14:37:47 +0200537 rid = role_info["_id"]
538 if not is_valid_uuid(rid): # Is this required?
539 role_obj_list = self.keystone.roles.list(name=rid)
tierno1f029d82019-06-13 22:37:04 +0000540 if not role_obj_list:
delacruzramo01b15d32019-07-02 14:37:47 +0200541 raise AuthconnNotFoundException("Role '{}' not found".format(rid))
542 rid = role_obj_list[0].id
543 self.keystone.roles.update(rid, name=role_info["name"], permissions=role_info.get("permissions"),
544 _admin=role_info.get("_admin"))
tierno1f029d82019-06-13 22:37:04 +0000545 except ClientException as e:
546 # self.logger.exception("Error during role update using keystone: {}".format(e))
547 raise AuthconnOperationException("Error during role updating using Keystone: {}".format(e))
548
tierno38dcfeb2019-06-10 16:44:00 +0000549 def get_project_list(self, filter_q=None):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100550 """
551 Get all the projects.
552
Eduardo Sousafa54cd92019-05-20 15:58:41 +0100553 :param filter_q: dictionary to filter project list.
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100554 :return: list of projects
555 """
556 try:
tiernoad6d5332020-02-19 14:29:49 +0000557 self._get_domains()
558 filter_name = filter_domain = None
tierno38dcfeb2019-06-10 16:44:00 +0000559 if filter_q:
560 filter_name = filter_q.get("name")
tiernoad6d5332020-02-19 14:29:49 +0000561 if filter_q.get("domain_name"):
562 filter_domain = self.domains_name2id.get(filter_q["domain_name"])
563 if filter_q.get("domain_id"):
564 filter_domain = filter_q["domain_id"]
565
566 projects = self.keystone.projects.list(name=filter_name, domain=filter_domain)
tierno38dcfeb2019-06-10 16:44:00 +0000567
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100568 projects = [{
569 "name": project.name,
delacruzramo01b15d32019-07-02 14:37:47 +0200570 "_id": project.id,
delacruzramo32bab472019-09-13 12:24:22 +0200571 "_admin": project.to_dict().get("_admin", {}), # TODO: REVISE
572 "quotas": project.to_dict().get("quotas", {}), # TODO: REVISE
tiernoad6d5332020-02-19 14:29:49 +0000573 "domain_name": self.domains_id2name.get(project.domain_id)
tierno38dcfeb2019-06-10 16:44:00 +0000574 } for project in projects]
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100575
tierno38dcfeb2019-06-10 16:44:00 +0000576 if filter_q and filter_q.get("_id"):
Eduardo Sousafa54cd92019-05-20 15:58:41 +0100577 projects = [project for project in projects
tierno38dcfeb2019-06-10 16:44:00 +0000578 if filter_q["_id"] == project["_id"]]
Eduardo Sousafa54cd92019-05-20 15:58:41 +0100579
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100580 return projects
tierno4015b472019-06-10 13:57:29 +0000581 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000582 # self.logger.exception("Error during user project listing using keystone: {}".format(e))
tierno4015b472019-06-10 13:57:29 +0000583 raise AuthException("Error during user project listing using Keystone: {}".format(e),
584 http_code=HTTPStatus.UNAUTHORIZED)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100585
delacruzramo01b15d32019-07-02 14:37:47 +0200586 def create_project(self, project_info):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100587 """
588 Create a project.
589
delacruzramo01b15d32019-07-02 14:37:47 +0200590 :param project_info: full project info.
tierno4015b472019-06-10 13:57:29 +0000591 :return: the internal id of the created project
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100592 :raises AuthconnOperationException: if project creation failed.
593 """
594 try:
tierno6486f742020-02-13 16:30:14 +0000595 result = self.keystone.projects.create(
596 project_info["name"],
tiernoad6d5332020-02-19 14:29:49 +0000597 domain=self._get_domain_id(project_info.get("domain_name", self.project_domain_name_list[0])),
tierno6486f742020-02-13 16:30:14 +0000598 _admin=project_info["_admin"],
599 quotas=project_info.get("quotas", {})
600 )
tierno4015b472019-06-10 13:57:29 +0000601 return result.id
602 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000603 # self.logger.exception("Error during project creation using keystone: {}".format(e))
tierno4015b472019-06-10 13:57:29 +0000604 raise AuthconnOperationException("Error during project creation using Keystone: {}".format(e))
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100605
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100606 def delete_project(self, project_id):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100607 """
608 Delete a project.
609
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100610 :param project_id: project identifier.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100611 :raises AuthconnOperationException: if project deletion failed.
612 """
613 try:
tierno38dcfeb2019-06-10 16:44:00 +0000614 # projects = self.keystone.projects.list()
615 # project_obj = [project for project in projects if project.id == project_id][0]
616 # result, _ = self.keystone.projects.delete(project_obj)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100617
tierno38dcfeb2019-06-10 16:44:00 +0000618 result, detail = self.keystone.projects.delete(project_id)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100619 if result.status_code != 204:
tierno38dcfeb2019-06-10 16:44:00 +0000620 raise ClientException("error {} {}".format(result.status_code, detail))
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100621
622 return True
tierno4015b472019-06-10 13:57:29 +0000623 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000624 # self.logger.exception("Error during project deletion using keystone: {}".format(e))
tierno4015b472019-06-10 13:57:29 +0000625 raise AuthconnOperationException("Error during project deletion using Keystone: {}".format(e))
626
delacruzramo01b15d32019-07-02 14:37:47 +0200627 def update_project(self, project_id, project_info):
tierno4015b472019-06-10 13:57:29 +0000628 """
629 Change the name of a project
630 :param project_id: project to be changed
delacruzramo01b15d32019-07-02 14:37:47 +0200631 :param project_info: full project info
tierno4015b472019-06-10 13:57:29 +0000632 :return: None
633 """
634 try:
delacruzramo32bab472019-09-13 12:24:22 +0200635 self.keystone.projects.update(project_id, name=project_info["name"],
636 _admin=project_info["_admin"],
637 quotas=project_info.get("quotas", {})
638 )
tierno4015b472019-06-10 13:57:29 +0000639 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000640 # self.logger.exception("Error during project update using keystone: {}".format(e))
delacruzramo01b15d32019-07-02 14:37:47 +0200641 raise AuthconnOperationException("Error during project update using Keystone: {}".format(e))
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100642
tiernoad6d5332020-02-19 14:29:49 +0000643 def assign_role_to_user(self, user_obj, project, role):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100644 """
645 Assigning a role to a user in a project.
646
tiernoad6d5332020-02-19 14:29:49 +0000647 :param user_obj: user object, obtained with keystone.users.get or list.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100648 :param project: project name.
649 :param role: role name.
650 :raises AuthconnOperationException: if role assignment failed.
651 """
652 try:
tiernoad6d5332020-02-19 14:29:49 +0000653 try:
Eduardo Sousa44603902019-06-04 08:10:32 +0100654 project_obj = self.keystone.projects.get(project)
tiernoad6d5332020-02-19 14:29:49 +0000655 except Exception:
tiernocf042d32019-06-13 09:06:40 +0000656 project_obj_list = self.keystone.projects.list(name=project)
657 if not project_obj_list:
658 raise AuthconnNotFoundException("Project '{}' not found".format(project))
659 project_obj = project_obj_list[0]
Eduardo Sousa44603902019-06-04 08:10:32 +0100660
tiernoad6d5332020-02-19 14:29:49 +0000661 try:
Eduardo Sousa44603902019-06-04 08:10:32 +0100662 role_obj = self.keystone.roles.get(role)
tiernoad6d5332020-02-19 14:29:49 +0000663 except Exception:
tiernocf042d32019-06-13 09:06:40 +0000664 role_obj_list = self.keystone.roles.list(name=role)
665 if not role_obj_list:
666 raise AuthconnNotFoundException("Role '{}' not found".format(role))
667 role_obj = role_obj_list[0]
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100668
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000669 self.keystone.roles.grant(role_obj, user=user_obj, project=project_obj)
tierno4015b472019-06-10 13:57:29 +0000670 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000671 # self.logger.exception("Error during user role assignment using keystone: {}".format(e))
tiernocf042d32019-06-13 09:06:40 +0000672 raise AuthconnOperationException("Error during role '{}' assignment to user '{}' and project '{}' using "
tiernoad6d5332020-02-19 14:29:49 +0000673 "Keystone: {}".format(role, user_obj.name, project, e))
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100674
tiernoad6d5332020-02-19 14:29:49 +0000675 def remove_role_from_user(self, user_obj, project, role):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100676 """
677 Remove a role from a user in a project.
678
tiernoad6d5332020-02-19 14:29:49 +0000679 :param user_obj: user object, obtained with keystone.users.get or list.
tiernocf042d32019-06-13 09:06:40 +0000680 :param project: project name or id.
681 :param role: role name or id.
682
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100683 :raises AuthconnOperationException: if role assignment revocation failed.
684 """
685 try:
tiernoad6d5332020-02-19 14:29:49 +0000686 try:
tiernocf042d32019-06-13 09:06:40 +0000687 project_obj = self.keystone.projects.get(project)
tiernoad6d5332020-02-19 14:29:49 +0000688 except Exception:
tiernocf042d32019-06-13 09:06:40 +0000689 project_obj_list = self.keystone.projects.list(name=project)
690 if not project_obj_list:
691 raise AuthconnNotFoundException("Project '{}' not found".format(project))
692 project_obj = project_obj_list[0]
693
tiernoad6d5332020-02-19 14:29:49 +0000694 try:
tiernocf042d32019-06-13 09:06:40 +0000695 role_obj = self.keystone.roles.get(role)
tiernoad6d5332020-02-19 14:29:49 +0000696 except Exception:
tiernocf042d32019-06-13 09:06:40 +0000697 role_obj_list = self.keystone.roles.list(name=role)
698 if not role_obj_list:
699 raise AuthconnNotFoundException("Role '{}' not found".format(role))
700 role_obj = role_obj_list[0]
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100701
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000702 self.keystone.roles.revoke(role_obj, user=user_obj, project=project_obj)
tierno4015b472019-06-10 13:57:29 +0000703 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000704 # self.logger.exception("Error during user role revocation using keystone: {}".format(e))
tiernocf042d32019-06-13 09:06:40 +0000705 raise AuthconnOperationException("Error during role '{}' revocation to user '{}' and project '{}' using "
tiernoad6d5332020-02-19 14:29:49 +0000706 "Keystone: {}".format(role, user_obj.name, project, e))