blob: a84b3d91117b884998233c02ff7c65452af070ac [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
garciadeblas4568a372021-03-24 09:19:48 +010028__author__ = (
29 "Eduardo Sousa <esousa@whitestack.com>, "
30 "Pedro de la Cruz Ramos <pdelacruzramos@altran.com>"
31)
Eduardo Sousa819d34c2018-07-31 01:20:02 +010032__date__ = "$27-jul-2018 23:59:59$"
33
garciadeblas4568a372021-03-24 09:19:48 +010034from osm_nbi.authconn import (
35 Authconn,
36 AuthException,
37 AuthconnOperationException,
38 AuthconnNotFoundException,
39 AuthconnConflictException,
40)
Eduardo Sousa819d34c2018-07-31 01:20:02 +010041
42import logging
Eduardo Sousa29933fc2018-11-14 06:36:35 +000043import requests
Eduardo Sousa44603902019-06-04 08:10:32 +010044import time
Eduardo Sousa819d34c2018-07-31 01:20:02 +010045from keystoneauth1 import session
46from keystoneauth1.identity import v3
47from keystoneauth1.exceptions.base import ClientException
Eduardo Sousa29933fc2018-11-14 06:36:35 +000048from keystoneauth1.exceptions.http import Conflict
Eduardo Sousa819d34c2018-07-31 01:20:02 +010049from keystoneclient.v3 import client
50from http import HTTPStatus
K Sai Kiran990ac462020-05-20 12:25:12 +053051from osm_nbi.validation import is_valid_uuid, validate_input, http_schema
Eduardo Sousa819d34c2018-07-31 01:20:02 +010052
53
54class AuthconnKeystone(Authconn):
tierno9e87a7f2020-03-23 09:24:10 +000055 def __init__(self, config, db, role_permissions):
56 Authconn.__init__(self, config, db, role_permissions)
Eduardo Sousa819d34c2018-07-31 01:20:02 +010057
58 self.logger = logging.getLogger("nbi.authenticator.keystone")
tiernoad6d5332020-02-19 14:29:49 +000059 self.domains_id2name = {}
60 self.domains_name2id = {}
Eduardo Sousa819d34c2018-07-31 01:20:02 +010061
K Sai Kiran990ac462020-05-20 12:25:12 +053062 self.auth_url = config.get("auth_url")
63 if config.get("auth_url"):
64 validate_input(self.auth_url, http_schema)
65 else:
garciadeblas4568a372021-03-24 09:19:48 +010066 self.auth_url = "http://{0}:{1}/v3".format(
67 config.get("auth_host", "keystone"), config.get("auth_port", "5000")
68 )
tierno6486f742020-02-13 16:30:14 +000069 self.user_domain_name_list = config.get("user_domain_name", "default")
70 self.user_domain_name_list = self.user_domain_name_list.split(",")
tiernoad6d5332020-02-19 14:29:49 +000071 # read only domain list
garciadeblas4568a372021-03-24 09:19:48 +010072 self.user_domain_ro_list = [
73 x[:-3] for x in self.user_domain_name_list if x.endswith(":ro")
74 ]
tiernoad6d5332020-02-19 14:29:49 +000075 # remove the ":ro"
garciadeblas4568a372021-03-24 09:19:48 +010076 self.user_domain_name_list = [
77 x if not x.endswith(":ro") else x[:-3] for x in self.user_domain_name_list
78 ]
tiernoad6d5332020-02-19 14:29:49 +000079
Eduardo Sousa819d34c2018-07-31 01:20:02 +010080 self.admin_project = config.get("service_project", "service")
81 self.admin_username = config.get("service_username", "nbi")
82 self.admin_password = config.get("service_password", "nbi")
tierno6486f742020-02-13 16:30:14 +000083 self.project_domain_name_list = config.get("project_domain_name", "default")
84 self.project_domain_name_list = self.project_domain_name_list.split(",")
85 if len(self.user_domain_name_list) != len(self.project_domain_name_list):
garciadeblas4568a372021-03-24 09:19:48 +010086 raise ValueError(
87 "Invalid configuration parameter fo authenticate. 'project_domain_name' and "
88 "'user_domain_name' must be a comma-separated list with the same size. Revise "
89 "configuration or/and 'OSMNBI_AUTHENTICATION_PROJECT_DOMAIN_NAME', "
90 "'OSMNBI_AUTHENTICATION_USER_DOMAIN_NAME' Variables"
91 )
Eduardo Sousa819d34c2018-07-31 01:20:02 +010092
Eduardo Sousa29933fc2018-11-14 06:36:35 +000093 # Waiting for Keystone to be up
94 available = None
95 counter = 300
96 while available is None:
97 time.sleep(1)
98 try:
99 result = requests.get(self.auth_url)
100 available = True if result.status_code == 200 else None
101 except Exception:
102 counter -= 1
103 if counter == 0:
104 raise AuthException("Keystone not available after 300s timeout")
105
garciadeblas4568a372021-03-24 09:19:48 +0100106 self.auth = v3.Password(
107 user_domain_name=self.user_domain_name_list[0],
108 username=self.admin_username,
109 password=self.admin_password,
110 project_domain_name=self.project_domain_name_list[0],
111 project_name=self.admin_project,
112 auth_url=self.auth_url,
113 )
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100114 self.sess = session.Session(auth=self.auth)
garciadeblas4568a372021-03-24 09:19:48 +0100115 self.keystone = client.Client(
116 session=self.sess, endpoint_override=self.auth_url
117 )
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100118
tierno6486f742020-02-13 16:30:14 +0000119 def authenticate(self, credentials, token_info=None):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100120 """
tierno701018c2019-06-25 11:13:14 +0000121 Authenticate a user using username/password or token_info, plus project
tierno6486f742020-02-13 16:30:14 +0000122 :param credentials: dictionary that contains:
123 username: name, id or None
124 password: password or None
125 project_id: name, id, or None. If None first found project will be used to get an scope token
126 project_domain_name: (Optional) To use a concrete domain for the project
127 user_domain_name: (Optional) To use a concrete domain for the project
128 other items are allowed and ignored
tierno701018c2019-06-25 11:13:14 +0000129 :param token_info: previous token_info to obtain authorization
tierno38dcfeb2019-06-10 16:44:00 +0000130 :return: the scoped token info or raises an exception. The token is a dictionary with:
131 _id: token string id,
132 username: username,
133 project_id: scoped_token project_id,
134 project_name: scoped_token project_name,
135 expires: epoch time when it expires,
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100136
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100137 """
tierno6486f742020-02-13 16:30:14 +0000138 username = None
139 user_id = None
140 project_id = None
141 project_name = None
142 if credentials.get("project_domain_name"):
garciadeblas4568a372021-03-24 09:19:48 +0100143 project_domain_name_list = (credentials["project_domain_name"],)
tierno6486f742020-02-13 16:30:14 +0000144 else:
145 project_domain_name_list = self.project_domain_name_list
146 if credentials.get("user_domain_name"):
garciadeblas4568a372021-03-24 09:19:48 +0100147 user_domain_name_list = (credentials["user_domain_name"],)
tierno6486f742020-02-13 16:30:14 +0000148 else:
149 user_domain_name_list = self.user_domain_name_list
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100150
tierno6486f742020-02-13 16:30:14 +0000151 for index, project_domain_name in enumerate(project_domain_name_list):
152 user_domain_name = user_domain_name_list[index]
153 try:
154 if credentials.get("username"):
155 if is_valid_uuid(credentials["username"]):
156 user_id = credentials["username"]
157 else:
158 username = credentials["username"]
159
160 # get an unscoped token firstly
161 unscoped_token = self.keystone.get_raw_token_from_identity_service(
162 auth_url=self.auth_url,
163 user_id=user_id,
164 username=username,
165 password=credentials.get("password"),
166 user_domain_name=user_domain_name,
garciadeblas4568a372021-03-24 09:19:48 +0100167 project_domain_name=project_domain_name,
168 )
tierno6486f742020-02-13 16:30:14 +0000169 elif token_info:
garciadeblas4568a372021-03-24 09:19:48 +0100170 unscoped_token = self.keystone.tokens.validate(
171 token=token_info.get("_id")
172 )
tierno38dcfeb2019-06-10 16:44:00 +0000173 else:
garciadeblas4568a372021-03-24 09:19:48 +0100174 raise AuthException(
175 "Provide credentials: username/password or Authorization Bearer token",
176 http_code=HTTPStatus.UNAUTHORIZED,
177 )
tierno38dcfeb2019-06-10 16:44:00 +0000178
tierno6486f742020-02-13 16:30:14 +0000179 if not credentials.get("project_id"):
180 # get first project for the user
garciadeblas4568a372021-03-24 09:19:48 +0100181 project_list = self.keystone.projects.list(
182 user=unscoped_token["user"]["id"]
183 )
tierno6486f742020-02-13 16:30:14 +0000184 if not project_list:
garciadeblas4568a372021-03-24 09:19:48 +0100185 raise AuthException(
186 "The user {} has not any project and cannot be used for authentication".format(
187 credentials.get("username")
188 ),
189 http_code=HTTPStatus.UNAUTHORIZED,
190 )
tierno6486f742020-02-13 16:30:14 +0000191 project_id = project_list[0].id
192 else:
193 if is_valid_uuid(credentials["project_id"]):
194 project_id = credentials["project_id"]
195 else:
196 project_name = credentials["project_id"]
197
198 scoped_token = self.keystone.get_raw_token_from_identity_service(
tierno38dcfeb2019-06-10 16:44:00 +0000199 auth_url=self.auth_url,
tierno6486f742020-02-13 16:30:14 +0000200 project_name=project_name,
201 project_id=project_id,
202 user_domain_name=user_domain_name,
203 project_domain_name=project_domain_name,
garciadeblas4568a372021-03-24 09:19:48 +0100204 token=unscoped_token["auth_token"],
205 )
tierno38dcfeb2019-06-10 16:44:00 +0000206
tierno6486f742020-02-13 16:30:14 +0000207 auth_token = {
208 "_id": scoped_token.auth_token,
209 "id": scoped_token.auth_token,
210 "user_id": scoped_token.user_id,
211 "username": scoped_token.username,
212 "project_id": scoped_token.project_id,
213 "project_name": scoped_token.project_name,
214 "project_domain_name": scoped_token.project_domain_name,
215 "user_domain_name": scoped_token.user_domain_name,
216 "expires": scoped_token.expires.timestamp(),
garciadeblas4568a372021-03-24 09:19:48 +0100217 "issued_at": scoped_token.issued.timestamp(),
tierno6486f742020-02-13 16:30:14 +0000218 }
tierno38dcfeb2019-06-10 16:44:00 +0000219
tierno6486f742020-02-13 16:30:14 +0000220 return auth_token
221 except ClientException as e:
garciadeblas4568a372021-03-24 09:19:48 +0100222 if (
223 index >= len(user_domain_name_list) - 1
224 or index >= len(project_domain_name_list) - 1
225 ):
tierno6486f742020-02-13 16:30:14 +0000226 # if last try, launch exception
227 # self.logger.exception("Error during user authentication using keystone: {}".format(e))
garciadeblas4568a372021-03-24 09:19:48 +0100228 raise AuthException(
229 "Error during user authentication using Keystone: {}".format(e),
230 http_code=HTTPStatus.UNAUTHORIZED,
231 )
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100232
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100233 def validate_token(self, token):
234 """
235 Check if the token is valid.
236
tierno701018c2019-06-25 11:13:14 +0000237 :param token: token id to be validated
tiernoa6bb45d2019-06-14 09:45:39 +0000238 :return: dictionary with information associated with the token:
239 "expires":
240 "_id": token_id,
241 "project_id": project_id,
242 "username": ,
243 "roles": list with dict containing {name, id}
244 If the token is not valid an exception is raised.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100245 """
246 if not token:
247 return
248
249 try:
250 token_info = self.keystone.tokens.validate(token=token)
tiernoa6bb45d2019-06-14 09:45:39 +0000251 ses = {
252 "_id": token_info["auth_token"],
tierno701018c2019-06-25 11:13:14 +0000253 "id": token_info["auth_token"],
tiernoa6bb45d2019-06-14 09:45:39 +0000254 "project_id": token_info["project"]["id"],
255 "project_name": token_info["project"]["name"],
256 "user_id": token_info["user"]["id"],
257 "username": token_info["user"]["name"],
258 "roles": token_info["roles"],
tierno701018c2019-06-25 11:13:14 +0000259 "expires": token_info.expires.timestamp(),
garciadeblas4568a372021-03-24 09:19:48 +0100260 "issued_at": token_info.issued.timestamp(),
tiernoa6bb45d2019-06-14 09:45:39 +0000261 }
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100262
tiernoa6bb45d2019-06-14 09:45:39 +0000263 return ses
tierno4015b472019-06-10 13:57:29 +0000264 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000265 # self.logger.exception("Error during token validation using keystone: {}".format(e))
garciadeblas4568a372021-03-24 09:19:48 +0100266 raise AuthException(
267 "Error during token validation using Keystone: {}".format(e),
268 http_code=HTTPStatus.UNAUTHORIZED,
269 )
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100270
271 def revoke_token(self, token):
272 """
273 Invalidate a token.
274
275 :param token: token to be revoked
276 """
277 try:
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000278 self.logger.info("Revoking token: " + token)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100279 self.keystone.tokens.revoke_token(token=token)
280
281 return True
tierno4015b472019-06-10 13:57:29 +0000282 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000283 # self.logger.exception("Error during token revocation using keystone: {}".format(e))
garciadeblas4568a372021-03-24 09:19:48 +0100284 raise AuthException(
285 "Error during token revocation using Keystone: {}".format(e),
286 http_code=HTTPStatus.UNAUTHORIZED,
287 )
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100288
tiernoad6d5332020-02-19 14:29:49 +0000289 def _get_domain_id(self, domain_name, fail_if_not_found=True):
290 """
291 Get the domain id from the domain_name
292 :param domain_name: Can be the name or id
293 :param fail_if_not_found: If False it returns None instead of raising an exception if not found
294 :return: str or None/exception if domain is not found
295 """
296 domain_id = self.domains_name2id.get(domain_name)
297 if not domain_id:
298 self._get_domains()
299 domain_id = self.domains_name2id.get(domain_name)
300 if not domain_id and domain_name in self.domains_id2name:
301 # domain_name is already an id
302 return domain_name
303 if not domain_id and fail_if_not_found:
garciadeblas4568a372021-03-24 09:19:48 +0100304 raise AuthconnNotFoundException(
305 "Domain {} cannot be found".format(domain_name)
306 )
tiernoad6d5332020-02-19 14:29:49 +0000307 return domain_id
308
309 def _get_domains(self):
310 """
311 Obtain a dictionary with domain_id to domain_name, stored at self.domains_id2name
312 and from domain_name to domain_id, sored at self.domains_name2id
313 :return: None. Exceptions are ignored
314 """
315 try:
316 domains = self.keystone.domains.list()
317 self.domains_id2name = {x.id: x.name for x in domains}
318 self.domains_name2id = {x.name: x.id for x in domains}
319 except Exception:
320 pass
321
delacruzramo01b15d32019-07-02 14:37:47 +0200322 def create_user(self, user_info):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100323 """
324 Create a user.
325
delacruzramo01b15d32019-07-02 14:37:47 +0200326 :param user_info: full user info.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100327 :raises AuthconnOperationException: if user creation failed.
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100328 :return: returns the id of the user in keystone.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100329 """
330 try:
garciadeblas4568a372021-03-24 09:19:48 +0100331 if (
332 user_info.get("domain_name")
333 and user_info["domain_name"] in self.user_domain_ro_list
334 ):
335 raise AuthconnConflictException(
336 "Cannot create a user in the read only domain {}".format(
337 user_info["domain_name"]
338 )
339 )
tiernoad6d5332020-02-19 14:29:49 +0000340
tierno6486f742020-02-13 16:30:14 +0000341 new_user = self.keystone.users.create(
garciadeblas4568a372021-03-24 09:19:48 +0100342 user_info["username"],
343 password=user_info["password"],
344 domain=self._get_domain_id(
345 user_info.get("domain_name", self.user_domain_name_list[0])
346 ),
347 _admin=user_info["_admin"],
348 )
delacruzramo01b15d32019-07-02 14:37:47 +0200349 if "project_role_mappings" in user_info.keys():
350 for mapping in user_info["project_role_mappings"]:
garciadeblas4568a372021-03-24 09:19:48 +0100351 self.assign_role_to_user(
352 new_user, mapping["project"], mapping["role"]
353 )
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100354 return {"username": new_user.name, "_id": new_user.id}
tiernocf042d32019-06-13 09:06:40 +0000355 except Conflict as e:
356 # self.logger.exception("Error during user creation using keystone: {}".format(e))
357 raise AuthconnOperationException(e, http_code=HTTPStatus.CONFLICT)
tierno4015b472019-06-10 13:57:29 +0000358 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000359 # self.logger.exception("Error during user creation using keystone: {}".format(e))
garciadeblas4568a372021-03-24 09:19:48 +0100360 raise AuthconnOperationException(
361 "Error during user creation using Keystone: {}".format(e)
362 )
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100363
delacruzramo01b15d32019-07-02 14:37:47 +0200364 def update_user(self, user_info):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100365 """
tiernocf042d32019-06-13 09:06:40 +0000366 Change the user name and/or password.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100367
delacruzramo01b15d32019-07-02 14:37:47 +0200368 :param user_info: user info modifications
tiernocf042d32019-06-13 09:06:40 +0000369 :raises AuthconnOperationException: if change failed.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100370 """
371 try:
delacruzramo01b15d32019-07-02 14:37:47 +0200372 user = user_info.get("_id") or user_info.get("username")
tiernoad6d5332020-02-19 14:29:49 +0000373 try:
374 user_obj = self.keystone.users.get(user)
375 except Exception:
376 user_obj = None
377 if not user_obj:
378 for user_domain in self.user_domain_name_list:
garciadeblas4568a372021-03-24 09:19:48 +0100379 domain_id = self._get_domain_id(
380 user_domain, fail_if_not_found=False
381 )
tiernoad6d5332020-02-19 14:29:49 +0000382 if not domain_id:
383 continue
garciadeblas4568a372021-03-24 09:19:48 +0100384 user_obj_list = self.keystone.users.list(
385 name=user, domain=domain_id
386 )
tiernoad6d5332020-02-19 14:29:49 +0000387 if user_obj_list:
388 user_obj = user_obj_list[0]
389 break
garciadeblas4568a372021-03-24 09:19:48 +0100390 else: # user not found
tiernoad6d5332020-02-19 14:29:49 +0000391 raise AuthconnNotFoundException("User '{}' not found".format(user))
392
delacruzramo01b15d32019-07-02 14:37:47 +0200393 user_id = user_obj.id
tiernoad6d5332020-02-19 14:29:49 +0000394 domain_id = user_obj.domain_id
395 domain_name = self.domains_id2name.get(domain_id)
396
397 if domain_name in self.user_domain_ro_list:
398 if user_info.get("password") or user_info.get("username"):
garciadeblas4568a372021-03-24 09:19:48 +0100399 raise AuthconnConflictException(
400 "Cannot update the user {} belonging to a read only domain {}".format(
401 user, domain_name
402 )
403 )
tiernoad6d5332020-02-19 14:29:49 +0000404
garciadeblas4568a372021-03-24 09:19:48 +0100405 elif (
406 user_info.get("password")
407 or user_info.get("username")
408 or user_info.get("add_project_role_mappings")
409 or user_info.get("remove_project_role_mappings")
410 ):
tiernoad6d5332020-02-19 14:29:49 +0000411 # if user_index>0, it is an external domain, that should not be updated
garciadeblas4568a372021-03-24 09:19:48 +0100412 ctime = (
413 user_obj._admin.get("created", 0)
414 if hasattr(user_obj, "_admin")
415 else 0
416 )
K Sai Kiran974276d2020-05-27 16:30:10 +0530417 try:
garciadeblas4568a372021-03-24 09:19:48 +0100418 self.keystone.users.update(
419 user_id,
420 password=user_info.get("password"),
421 name=user_info.get("username"),
422 _admin={"created": ctime, "modified": time.time()},
423 )
K Sai Kiran974276d2020-05-27 16:30:10 +0530424 except Exception as e:
425 if user_info.get("username") or user_info.get("password"):
garciadeblas4568a372021-03-24 09:19:48 +0100426 raise AuthconnOperationException(
427 "Error during username/password change: {}".format(str(e))
428 )
429 self.logger.error(
430 "Error during updating user profile: {}".format(str(e))
431 )
tiernoad6d5332020-02-19 14:29:49 +0000432
delacruzramo01b15d32019-07-02 14:37:47 +0200433 for mapping in user_info.get("remove_project_role_mappings", []):
garciadeblas4568a372021-03-24 09:19:48 +0100434 self.remove_role_from_user(
435 user_obj, mapping["project"], mapping["role"]
436 )
delacruzramo01b15d32019-07-02 14:37:47 +0200437 for mapping in user_info.get("add_project_role_mappings", []):
tiernoad6d5332020-02-19 14:29:49 +0000438 self.assign_role_to_user(user_obj, mapping["project"], mapping["role"])
tierno4015b472019-06-10 13:57:29 +0000439 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000440 # self.logger.exception("Error during user password/name update using keystone: {}".format(e))
garciadeblas4568a372021-03-24 09:19:48 +0100441 raise AuthconnOperationException(
442 "Error during user update using Keystone: {}".format(e)
443 )
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100444
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100445 def delete_user(self, user_id):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100446 """
447 Delete user.
448
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100449 :param user_id: user identifier.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100450 :raises AuthconnOperationException: if user deletion failed.
451 """
452 try:
tiernoad6d5332020-02-19 14:29:49 +0000453 user_obj = self.keystone.users.get(user_id)
454 domain_id = user_obj.domain_id
455 domain_name = self.domains_id2name.get(domain_id)
456 if domain_name in self.user_domain_ro_list:
garciadeblas4568a372021-03-24 09:19:48 +0100457 raise AuthconnConflictException(
458 "Cannot delete user {} belonging to a read only domain {}".format(
459 user_id, domain_name
460 )
461 )
tiernoad6d5332020-02-19 14:29:49 +0000462
tierno38dcfeb2019-06-10 16:44:00 +0000463 result, detail = self.keystone.users.delete(user_id)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100464 if result.status_code != 204:
tierno38dcfeb2019-06-10 16:44:00 +0000465 raise ClientException("error {} {}".format(result.status_code, detail))
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100466 return True
tierno4015b472019-06-10 13:57:29 +0000467 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000468 # self.logger.exception("Error during user deletion using keystone: {}".format(e))
garciadeblas4568a372021-03-24 09:19:48 +0100469 raise AuthconnOperationException(
470 "Error during user deletion using Keystone: {}".format(e)
471 )
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100472
tiernocf042d32019-06-13 09:06:40 +0000473 def get_user_list(self, filter_q=None):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100474 """
475 Get user list.
476
tiernoad6d5332020-02-19 14:29:49 +0000477 :param filter_q: dictionary to filter user list by one or several
478 _id:
479 name (username is also admitted). If a user id is equal to the filter name, it is also provided
480 domain_id, domain_name
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100481 :return: returns a list of users.
482 """
483 try:
tiernoad6d5332020-02-19 14:29:49 +0000484 self._get_domains()
485 filter_name = filter_domain = None
tiernocf042d32019-06-13 09:06:40 +0000486 if filter_q:
487 filter_name = filter_q.get("name") or filter_q.get("username")
tiernoad6d5332020-02-19 14:29:49 +0000488 if filter_q.get("domain_name"):
garciadeblas4568a372021-03-24 09:19:48 +0100489 filter_domain = self._get_domain_id(
490 filter_q["domain_name"], fail_if_not_found=False
491 )
tiernoad6d5332020-02-19 14:29:49 +0000492 # If domain is not found, use the same name to obtain an empty list
493 filter_domain = filter_domain or filter_q["domain_name"]
494 if filter_q.get("domain_id"):
495 filter_domain = filter_q["domain_id"]
496
497 users = self.keystone.users.list(name=filter_name, domain=filter_domain)
498 # get users from user_domain_name_list[1:], because it will not be provided in case of LDAP
499 if filter_domain is None and len(self.user_domain_name_list) > 1:
500 for user_domain in self.user_domain_name_list[1:]:
garciadeblas4568a372021-03-24 09:19:48 +0100501 domain_id = self._get_domain_id(
502 user_domain, fail_if_not_found=False
503 )
tiernoad6d5332020-02-19 14:29:49 +0000504 if not domain_id:
505 continue
506 # find if users of this domain are already provided. In this case ignore
507 for u in users:
508 if u.domain_id == domain_id:
509 break
510 else:
garciadeblas4568a372021-03-24 09:19:48 +0100511 users += self.keystone.users.list(
512 name=filter_name, domain=domain_id
513 )
tiernoad6d5332020-02-19 14:29:49 +0000514
515 # if filter name matches a user id, provide it also
516 if filter_name:
517 try:
518 user_obj = self.keystone.users.get(filter_name)
519 if user_obj not in users:
520 users.append(user_obj)
521 except Exception:
522 pass
523
garciadeblas4568a372021-03-24 09:19:48 +0100524 users = [
525 {
526 "username": user.name,
527 "_id": user.id,
528 "id": user.id,
529 "_admin": user.to_dict().get("_admin", {}), # TODO: REVISE
530 "domain_name": self.domains_id2name.get(user.domain_id),
531 }
532 for user in users
533 if user.name != self.admin_username
534 ]
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100535
tiernocf042d32019-06-13 09:06:40 +0000536 if filter_q and filter_q.get("_id"):
537 users = [user for user in users if filter_q["_id"] == user["_id"]]
Eduardo Sousa2d5a5152019-05-20 15:41:54 +0100538
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100539 for user in users:
tierno1546f2a2019-08-20 15:38:11 +0000540 user["project_role_mappings"] = []
541 user["projects"] = []
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100542 projects = self.keystone.projects.list(user=user["_id"])
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100543 for project in projects:
tierno1546f2a2019-08-20 15:38:11 +0000544 user["projects"].append(project.name)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100545
garciadeblas4568a372021-03-24 09:19:48 +0100546 roles = self.keystone.roles.list(
547 user=user["_id"], project=project.id
548 )
tierno1546f2a2019-08-20 15:38:11 +0000549 for role in roles:
550 prm = {
551 "project": project.id,
552 "project_name": project.name,
553 "role_name": role.name,
554 "role": role.id,
555 }
556 user["project_role_mappings"].append(prm)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100557
558 return users
tierno4015b472019-06-10 13:57:29 +0000559 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000560 # self.logger.exception("Error during user listing using keystone: {}".format(e))
garciadeblas4568a372021-03-24 09:19:48 +0100561 raise AuthconnOperationException(
562 "Error during user listing using Keystone: {}".format(e)
563 )
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100564
tierno1f029d82019-06-13 22:37:04 +0000565 def get_role_list(self, filter_q=None):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100566 """
567 Get role list.
568
tierno1f029d82019-06-13 22:37:04 +0000569 :param filter_q: dictionary to filter role list by _id and/or name.
Eduardo Sousa37de0912019-05-23 02:17:22 +0100570 :return: returns the list of roles.
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100571 """
572 try:
tierno1f029d82019-06-13 22:37:04 +0000573 filter_name = None
574 if filter_q:
575 filter_name = filter_q.get("name")
576 roles_list = self.keystone.roles.list(name=filter_name)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100577
garciadeblas4568a372021-03-24 09:19:48 +0100578 roles = [
579 {
580 "name": role.name,
581 "_id": role.id,
582 "_admin": role.to_dict().get("_admin", {}),
583 "permissions": role.to_dict().get("permissions", {}),
584 }
585 for role in roles_list
586 if role.name != "service"
587 ]
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100588
tierno1f029d82019-06-13 22:37:04 +0000589 if filter_q and filter_q.get("_id"):
590 roles = [role for role in roles if filter_q["_id"] == role["_id"]]
591
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100592 return roles
tierno4015b472019-06-10 13:57:29 +0000593 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000594 # self.logger.exception("Error during user role listing using keystone: {}".format(e))
garciadeblas4568a372021-03-24 09:19:48 +0100595 raise AuthException(
596 "Error during user role listing using Keystone: {}".format(e),
597 http_code=HTTPStatus.UNAUTHORIZED,
598 )
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100599
delacruzramo01b15d32019-07-02 14:37:47 +0200600 def create_role(self, role_info):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100601 """
602 Create a role.
603
delacruzramo01b15d32019-07-02 14:37:47 +0200604 :param role_info: full role info.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100605 :raises AuthconnOperationException: if role creation failed.
606 """
607 try:
garciadeblas4568a372021-03-24 09:19:48 +0100608 result = self.keystone.roles.create(
609 role_info["name"],
610 permissions=role_info.get("permissions"),
611 _admin=role_info.get("_admin"),
612 )
tierno1f029d82019-06-13 22:37:04 +0000613 return result.id
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000614 except Conflict as ex:
tierno1f029d82019-06-13 22:37:04 +0000615 raise AuthconnConflictException(str(ex))
tierno4015b472019-06-10 13:57:29 +0000616 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000617 # self.logger.exception("Error during role creation using keystone: {}".format(e))
garciadeblas4568a372021-03-24 09:19:48 +0100618 raise AuthconnOperationException(
619 "Error during role creation using Keystone: {}".format(e)
620 )
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100621
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100622 def delete_role(self, role_id):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100623 """
624 Delete a role.
625
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100626 :param role_id: role identifier.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100627 :raises AuthconnOperationException: if role deletion failed.
628 """
629 try:
tierno1f029d82019-06-13 22:37:04 +0000630 result, detail = self.keystone.roles.delete(role_id)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100631
632 if result.status_code != 204:
tierno38dcfeb2019-06-10 16:44:00 +0000633 raise ClientException("error {} {}".format(result.status_code, detail))
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100634
635 return True
tierno4015b472019-06-10 13:57:29 +0000636 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000637 # self.logger.exception("Error during role deletion using keystone: {}".format(e))
garciadeblas4568a372021-03-24 09:19:48 +0100638 raise AuthconnOperationException(
639 "Error during role deletion using Keystone: {}".format(e)
640 )
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100641
delacruzramo01b15d32019-07-02 14:37:47 +0200642 def update_role(self, role_info):
tierno1f029d82019-06-13 22:37:04 +0000643 """
644 Change the name of a role
delacruzramo01b15d32019-07-02 14:37:47 +0200645 :param role_info: full role info
tierno1f029d82019-06-13 22:37:04 +0000646 :return: None
647 """
648 try:
delacruzramo01b15d32019-07-02 14:37:47 +0200649 rid = role_info["_id"]
garciadeblas4568a372021-03-24 09:19:48 +0100650 if not is_valid_uuid(rid): # Is this required?
delacruzramo01b15d32019-07-02 14:37:47 +0200651 role_obj_list = self.keystone.roles.list(name=rid)
tierno1f029d82019-06-13 22:37:04 +0000652 if not role_obj_list:
delacruzramo01b15d32019-07-02 14:37:47 +0200653 raise AuthconnNotFoundException("Role '{}' not found".format(rid))
654 rid = role_obj_list[0].id
garciadeblas4568a372021-03-24 09:19:48 +0100655 self.keystone.roles.update(
656 rid,
657 name=role_info["name"],
658 permissions=role_info.get("permissions"),
659 _admin=role_info.get("_admin"),
660 )
tierno1f029d82019-06-13 22:37:04 +0000661 except ClientException as e:
662 # self.logger.exception("Error during role update using keystone: {}".format(e))
garciadeblas4568a372021-03-24 09:19:48 +0100663 raise AuthconnOperationException(
664 "Error during role updating using Keystone: {}".format(e)
665 )
tierno1f029d82019-06-13 22:37:04 +0000666
tierno38dcfeb2019-06-10 16:44:00 +0000667 def get_project_list(self, filter_q=None):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100668 """
669 Get all the projects.
670
Eduardo Sousafa54cd92019-05-20 15:58:41 +0100671 :param filter_q: dictionary to filter project list.
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100672 :return: list of projects
673 """
674 try:
tiernoad6d5332020-02-19 14:29:49 +0000675 self._get_domains()
676 filter_name = filter_domain = None
tierno38dcfeb2019-06-10 16:44:00 +0000677 if filter_q:
678 filter_name = filter_q.get("name")
tiernoad6d5332020-02-19 14:29:49 +0000679 if filter_q.get("domain_name"):
680 filter_domain = self.domains_name2id.get(filter_q["domain_name"])
681 if filter_q.get("domain_id"):
682 filter_domain = filter_q["domain_id"]
683
garciadeblas4568a372021-03-24 09:19:48 +0100684 projects = self.keystone.projects.list(
685 name=filter_name, domain=filter_domain
686 )
tierno38dcfeb2019-06-10 16:44:00 +0000687
garciadeblas4568a372021-03-24 09:19:48 +0100688 projects = [
689 {
690 "name": project.name,
691 "_id": project.id,
692 "_admin": project.to_dict().get("_admin", {}), # TODO: REVISE
693 "quotas": project.to_dict().get("quotas", {}), # TODO: REVISE
694 "domain_name": self.domains_id2name.get(project.domain_id),
695 }
696 for project in projects
697 ]
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100698
tierno38dcfeb2019-06-10 16:44:00 +0000699 if filter_q and filter_q.get("_id"):
garciadeblas4568a372021-03-24 09:19:48 +0100700 projects = [
701 project for project in projects if filter_q["_id"] == project["_id"]
702 ]
Eduardo Sousafa54cd92019-05-20 15:58:41 +0100703
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100704 return projects
tierno4015b472019-06-10 13:57:29 +0000705 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000706 # self.logger.exception("Error during user project listing using keystone: {}".format(e))
garciadeblas4568a372021-03-24 09:19:48 +0100707 raise AuthException(
708 "Error during user project listing using Keystone: {}".format(e),
709 http_code=HTTPStatus.UNAUTHORIZED,
710 )
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100711
delacruzramo01b15d32019-07-02 14:37:47 +0200712 def create_project(self, project_info):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100713 """
714 Create a project.
715
delacruzramo01b15d32019-07-02 14:37:47 +0200716 :param project_info: full project info.
tierno4015b472019-06-10 13:57:29 +0000717 :return: the internal id of the created project
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100718 :raises AuthconnOperationException: if project creation failed.
719 """
720 try:
tierno6486f742020-02-13 16:30:14 +0000721 result = self.keystone.projects.create(
722 project_info["name"],
garciadeblas4568a372021-03-24 09:19:48 +0100723 domain=self._get_domain_id(
724 project_info.get("domain_name", self.project_domain_name_list[0])
725 ),
tierno6486f742020-02-13 16:30:14 +0000726 _admin=project_info["_admin"],
garciadeblas4568a372021-03-24 09:19:48 +0100727 quotas=project_info.get("quotas", {}),
tierno6486f742020-02-13 16:30:14 +0000728 )
tierno4015b472019-06-10 13:57:29 +0000729 return result.id
730 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000731 # self.logger.exception("Error during project creation using keystone: {}".format(e))
garciadeblas4568a372021-03-24 09:19:48 +0100732 raise AuthconnOperationException(
733 "Error during project creation using Keystone: {}".format(e)
734 )
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100735
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100736 def delete_project(self, project_id):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100737 """
738 Delete a project.
739
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100740 :param project_id: project identifier.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100741 :raises AuthconnOperationException: if project deletion failed.
742 """
743 try:
tierno38dcfeb2019-06-10 16:44:00 +0000744 # projects = self.keystone.projects.list()
745 # project_obj = [project for project in projects if project.id == project_id][0]
746 # result, _ = self.keystone.projects.delete(project_obj)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100747
tierno38dcfeb2019-06-10 16:44:00 +0000748 result, detail = self.keystone.projects.delete(project_id)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100749 if result.status_code != 204:
tierno38dcfeb2019-06-10 16:44:00 +0000750 raise ClientException("error {} {}".format(result.status_code, detail))
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100751
752 return True
tierno4015b472019-06-10 13:57:29 +0000753 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000754 # self.logger.exception("Error during project deletion using keystone: {}".format(e))
garciadeblas4568a372021-03-24 09:19:48 +0100755 raise AuthconnOperationException(
756 "Error during project deletion using Keystone: {}".format(e)
757 )
tierno4015b472019-06-10 13:57:29 +0000758
delacruzramo01b15d32019-07-02 14:37:47 +0200759 def update_project(self, project_id, project_info):
tierno4015b472019-06-10 13:57:29 +0000760 """
761 Change the name of a project
762 :param project_id: project to be changed
delacruzramo01b15d32019-07-02 14:37:47 +0200763 :param project_info: full project info
tierno4015b472019-06-10 13:57:29 +0000764 :return: None
765 """
766 try:
garciadeblas4568a372021-03-24 09:19:48 +0100767 self.keystone.projects.update(
768 project_id,
769 name=project_info["name"],
770 _admin=project_info["_admin"],
771 quotas=project_info.get("quotas", {}),
772 )
tierno4015b472019-06-10 13:57:29 +0000773 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000774 # self.logger.exception("Error during project update using keystone: {}".format(e))
garciadeblas4568a372021-03-24 09:19:48 +0100775 raise AuthconnOperationException(
776 "Error during project update using Keystone: {}".format(e)
777 )
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100778
tiernoad6d5332020-02-19 14:29:49 +0000779 def assign_role_to_user(self, user_obj, project, role):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100780 """
781 Assigning a role to a user in a project.
782
tiernoad6d5332020-02-19 14:29:49 +0000783 :param user_obj: user object, obtained with keystone.users.get or list.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100784 :param project: project name.
785 :param role: role name.
786 :raises AuthconnOperationException: if role assignment failed.
787 """
788 try:
tiernoad6d5332020-02-19 14:29:49 +0000789 try:
Eduardo Sousa44603902019-06-04 08:10:32 +0100790 project_obj = self.keystone.projects.get(project)
tiernoad6d5332020-02-19 14:29:49 +0000791 except Exception:
tiernocf042d32019-06-13 09:06:40 +0000792 project_obj_list = self.keystone.projects.list(name=project)
793 if not project_obj_list:
garciadeblas4568a372021-03-24 09:19:48 +0100794 raise AuthconnNotFoundException(
795 "Project '{}' not found".format(project)
796 )
tiernocf042d32019-06-13 09:06:40 +0000797 project_obj = project_obj_list[0]
Eduardo Sousa44603902019-06-04 08:10:32 +0100798
tiernoad6d5332020-02-19 14:29:49 +0000799 try:
Eduardo Sousa44603902019-06-04 08:10:32 +0100800 role_obj = self.keystone.roles.get(role)
tiernoad6d5332020-02-19 14:29:49 +0000801 except Exception:
tiernocf042d32019-06-13 09:06:40 +0000802 role_obj_list = self.keystone.roles.list(name=role)
803 if not role_obj_list:
804 raise AuthconnNotFoundException("Role '{}' not found".format(role))
805 role_obj = role_obj_list[0]
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100806
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000807 self.keystone.roles.grant(role_obj, user=user_obj, project=project_obj)
tierno4015b472019-06-10 13:57:29 +0000808 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000809 # self.logger.exception("Error during user role assignment using keystone: {}".format(e))
garciadeblas4568a372021-03-24 09:19:48 +0100810 raise AuthconnOperationException(
811 "Error during role '{}' assignment to user '{}' and project '{}' using "
812 "Keystone: {}".format(role, user_obj.name, project, e)
813 )
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100814
tiernoad6d5332020-02-19 14:29:49 +0000815 def remove_role_from_user(self, user_obj, project, role):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100816 """
817 Remove a role from a user in a project.
818
tiernoad6d5332020-02-19 14:29:49 +0000819 :param user_obj: user object, obtained with keystone.users.get or list.
tiernocf042d32019-06-13 09:06:40 +0000820 :param project: project name or id.
821 :param role: role name or id.
822
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100823 :raises AuthconnOperationException: if role assignment revocation failed.
824 """
825 try:
tiernoad6d5332020-02-19 14:29:49 +0000826 try:
tiernocf042d32019-06-13 09:06:40 +0000827 project_obj = self.keystone.projects.get(project)
tiernoad6d5332020-02-19 14:29:49 +0000828 except Exception:
tiernocf042d32019-06-13 09:06:40 +0000829 project_obj_list = self.keystone.projects.list(name=project)
830 if not project_obj_list:
garciadeblas4568a372021-03-24 09:19:48 +0100831 raise AuthconnNotFoundException(
832 "Project '{}' not found".format(project)
833 )
tiernocf042d32019-06-13 09:06:40 +0000834 project_obj = project_obj_list[0]
835
tiernoad6d5332020-02-19 14:29:49 +0000836 try:
tiernocf042d32019-06-13 09:06:40 +0000837 role_obj = self.keystone.roles.get(role)
tiernoad6d5332020-02-19 14:29:49 +0000838 except Exception:
tiernocf042d32019-06-13 09:06:40 +0000839 role_obj_list = self.keystone.roles.list(name=role)
840 if not role_obj_list:
841 raise AuthconnNotFoundException("Role '{}' not found".format(role))
842 role_obj = role_obj_list[0]
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100843
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000844 self.keystone.roles.revoke(role_obj, user=user_obj, project=project_obj)
tierno4015b472019-06-10 13:57:29 +0000845 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000846 # self.logger.exception("Error during user role revocation using keystone: {}".format(e))
garciadeblas4568a372021-03-24 09:19:48 +0100847 raise AuthconnOperationException(
848 "Error during role '{}' revocation to user '{}' and project '{}' using "
849 "Keystone: {}".format(role, user_obj.name, project, e)
850 )