blob: 5e344855dd3129ce85e69966bd88754172c9c7a6 [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:
tiernoad6d5332020-02-19 14:29:49 +0000331
garciadeblas4568a372021-03-24 09:19:48 +0100332 if (
333 user_info.get("domain_name")
334 and user_info["domain_name"] in self.user_domain_ro_list
335 ):
336 raise AuthconnConflictException(
337 "Cannot create a user in the read only domain {}".format(
338 user_info["domain_name"]
339 )
340 )
tiernoad6d5332020-02-19 14:29:49 +0000341
tierno6486f742020-02-13 16:30:14 +0000342 new_user = self.keystone.users.create(
garciadeblas4568a372021-03-24 09:19:48 +0100343 user_info["username"],
344 password=user_info["password"],
345 domain=self._get_domain_id(
346 user_info.get("domain_name", self.user_domain_name_list[0])
347 ),
348 _admin=user_info["_admin"],
349 )
delacruzramo01b15d32019-07-02 14:37:47 +0200350 if "project_role_mappings" in user_info.keys():
351 for mapping in user_info["project_role_mappings"]:
garciadeblas4568a372021-03-24 09:19:48 +0100352 self.assign_role_to_user(
353 new_user, mapping["project"], mapping["role"]
354 )
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100355 return {"username": new_user.name, "_id": new_user.id}
tiernocf042d32019-06-13 09:06:40 +0000356 except Conflict as e:
357 # self.logger.exception("Error during user creation using keystone: {}".format(e))
358 raise AuthconnOperationException(e, http_code=HTTPStatus.CONFLICT)
tierno4015b472019-06-10 13:57:29 +0000359 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000360 # self.logger.exception("Error during user creation using keystone: {}".format(e))
garciadeblas4568a372021-03-24 09:19:48 +0100361 raise AuthconnOperationException(
362 "Error during user creation using Keystone: {}".format(e)
363 )
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100364
delacruzramo01b15d32019-07-02 14:37:47 +0200365 def update_user(self, user_info):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100366 """
tiernocf042d32019-06-13 09:06:40 +0000367 Change the user name and/or password.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100368
delacruzramo01b15d32019-07-02 14:37:47 +0200369 :param user_info: user info modifications
tiernocf042d32019-06-13 09:06:40 +0000370 :raises AuthconnOperationException: if change failed.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100371 """
372 try:
delacruzramo01b15d32019-07-02 14:37:47 +0200373 user = user_info.get("_id") or user_info.get("username")
tiernoad6d5332020-02-19 14:29:49 +0000374 try:
375 user_obj = self.keystone.users.get(user)
376 except Exception:
377 user_obj = None
378 if not user_obj:
379 for user_domain in self.user_domain_name_list:
garciadeblas4568a372021-03-24 09:19:48 +0100380 domain_id = self._get_domain_id(
381 user_domain, fail_if_not_found=False
382 )
tiernoad6d5332020-02-19 14:29:49 +0000383 if not domain_id:
384 continue
garciadeblas4568a372021-03-24 09:19:48 +0100385 user_obj_list = self.keystone.users.list(
386 name=user, domain=domain_id
387 )
tiernoad6d5332020-02-19 14:29:49 +0000388 if user_obj_list:
389 user_obj = user_obj_list[0]
390 break
garciadeblas4568a372021-03-24 09:19:48 +0100391 else: # user not found
tiernoad6d5332020-02-19 14:29:49 +0000392 raise AuthconnNotFoundException("User '{}' not found".format(user))
393
delacruzramo01b15d32019-07-02 14:37:47 +0200394 user_id = user_obj.id
tiernoad6d5332020-02-19 14:29:49 +0000395 domain_id = user_obj.domain_id
396 domain_name = self.domains_id2name.get(domain_id)
397
398 if domain_name in self.user_domain_ro_list:
399 if user_info.get("password") or user_info.get("username"):
garciadeblas4568a372021-03-24 09:19:48 +0100400 raise AuthconnConflictException(
401 "Cannot update the user {} belonging to a read only domain {}".format(
402 user, domain_name
403 )
404 )
tiernoad6d5332020-02-19 14:29:49 +0000405
garciadeblas4568a372021-03-24 09:19:48 +0100406 elif (
407 user_info.get("password")
408 or user_info.get("username")
409 or user_info.get("add_project_role_mappings")
410 or user_info.get("remove_project_role_mappings")
411 ):
tiernoad6d5332020-02-19 14:29:49 +0000412 # if user_index>0, it is an external domain, that should not be updated
garciadeblas4568a372021-03-24 09:19:48 +0100413 ctime = (
414 user_obj._admin.get("created", 0)
415 if hasattr(user_obj, "_admin")
416 else 0
417 )
K Sai Kiran974276d2020-05-27 16:30:10 +0530418 try:
garciadeblas4568a372021-03-24 09:19:48 +0100419 self.keystone.users.update(
420 user_id,
421 password=user_info.get("password"),
422 name=user_info.get("username"),
423 _admin={"created": ctime, "modified": time.time()},
424 )
K Sai Kiran974276d2020-05-27 16:30:10 +0530425 except Exception as e:
426 if user_info.get("username") or user_info.get("password"):
garciadeblas4568a372021-03-24 09:19:48 +0100427 raise AuthconnOperationException(
428 "Error during username/password change: {}".format(str(e))
429 )
430 self.logger.error(
431 "Error during updating user profile: {}".format(str(e))
432 )
tiernoad6d5332020-02-19 14:29:49 +0000433
delacruzramo01b15d32019-07-02 14:37:47 +0200434 for mapping in user_info.get("remove_project_role_mappings", []):
garciadeblas4568a372021-03-24 09:19:48 +0100435 self.remove_role_from_user(
436 user_obj, mapping["project"], mapping["role"]
437 )
delacruzramo01b15d32019-07-02 14:37:47 +0200438 for mapping in user_info.get("add_project_role_mappings", []):
tiernoad6d5332020-02-19 14:29:49 +0000439 self.assign_role_to_user(user_obj, mapping["project"], mapping["role"])
tierno4015b472019-06-10 13:57:29 +0000440 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000441 # self.logger.exception("Error during user password/name update using keystone: {}".format(e))
garciadeblas4568a372021-03-24 09:19:48 +0100442 raise AuthconnOperationException(
443 "Error during user update using Keystone: {}".format(e)
444 )
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100445
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100446 def delete_user(self, user_id):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100447 """
448 Delete user.
449
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100450 :param user_id: user identifier.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100451 :raises AuthconnOperationException: if user deletion failed.
452 """
453 try:
tiernoad6d5332020-02-19 14:29:49 +0000454 user_obj = self.keystone.users.get(user_id)
455 domain_id = user_obj.domain_id
456 domain_name = self.domains_id2name.get(domain_id)
457 if domain_name in self.user_domain_ro_list:
garciadeblas4568a372021-03-24 09:19:48 +0100458 raise AuthconnConflictException(
459 "Cannot delete user {} belonging to a read only domain {}".format(
460 user_id, domain_name
461 )
462 )
tiernoad6d5332020-02-19 14:29:49 +0000463
tierno38dcfeb2019-06-10 16:44:00 +0000464 result, detail = self.keystone.users.delete(user_id)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100465 if result.status_code != 204:
tierno38dcfeb2019-06-10 16:44:00 +0000466 raise ClientException("error {} {}".format(result.status_code, detail))
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100467 return True
tierno4015b472019-06-10 13:57:29 +0000468 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000469 # self.logger.exception("Error during user deletion using keystone: {}".format(e))
garciadeblas4568a372021-03-24 09:19:48 +0100470 raise AuthconnOperationException(
471 "Error during user deletion using Keystone: {}".format(e)
472 )
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100473
tiernocf042d32019-06-13 09:06:40 +0000474 def get_user_list(self, filter_q=None):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100475 """
476 Get user list.
477
tiernoad6d5332020-02-19 14:29:49 +0000478 :param filter_q: dictionary to filter user list by one or several
479 _id:
480 name (username is also admitted). If a user id is equal to the filter name, it is also provided
481 domain_id, domain_name
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100482 :return: returns a list of users.
483 """
484 try:
tiernoad6d5332020-02-19 14:29:49 +0000485 self._get_domains()
486 filter_name = filter_domain = None
tiernocf042d32019-06-13 09:06:40 +0000487 if filter_q:
488 filter_name = filter_q.get("name") or filter_q.get("username")
tiernoad6d5332020-02-19 14:29:49 +0000489 if filter_q.get("domain_name"):
garciadeblas4568a372021-03-24 09:19:48 +0100490 filter_domain = self._get_domain_id(
491 filter_q["domain_name"], fail_if_not_found=False
492 )
tiernoad6d5332020-02-19 14:29:49 +0000493 # If domain is not found, use the same name to obtain an empty list
494 filter_domain = filter_domain or filter_q["domain_name"]
495 if filter_q.get("domain_id"):
496 filter_domain = filter_q["domain_id"]
497
498 users = self.keystone.users.list(name=filter_name, domain=filter_domain)
499 # get users from user_domain_name_list[1:], because it will not be provided in case of LDAP
500 if filter_domain is None and len(self.user_domain_name_list) > 1:
501 for user_domain in self.user_domain_name_list[1:]:
garciadeblas4568a372021-03-24 09:19:48 +0100502 domain_id = self._get_domain_id(
503 user_domain, fail_if_not_found=False
504 )
tiernoad6d5332020-02-19 14:29:49 +0000505 if not domain_id:
506 continue
507 # find if users of this domain are already provided. In this case ignore
508 for u in users:
509 if u.domain_id == domain_id:
510 break
511 else:
garciadeblas4568a372021-03-24 09:19:48 +0100512 users += self.keystone.users.list(
513 name=filter_name, domain=domain_id
514 )
tiernoad6d5332020-02-19 14:29:49 +0000515
516 # if filter name matches a user id, provide it also
517 if filter_name:
518 try:
519 user_obj = self.keystone.users.get(filter_name)
520 if user_obj not in users:
521 users.append(user_obj)
522 except Exception:
523 pass
524
garciadeblas4568a372021-03-24 09:19:48 +0100525 users = [
526 {
527 "username": user.name,
528 "_id": user.id,
529 "id": user.id,
530 "_admin": user.to_dict().get("_admin", {}), # TODO: REVISE
531 "domain_name": self.domains_id2name.get(user.domain_id),
532 }
533 for user in users
534 if user.name != self.admin_username
535 ]
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100536
tiernocf042d32019-06-13 09:06:40 +0000537 if filter_q and filter_q.get("_id"):
538 users = [user for user in users if filter_q["_id"] == user["_id"]]
Eduardo Sousa2d5a5152019-05-20 15:41:54 +0100539
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100540 for user in users:
tierno1546f2a2019-08-20 15:38:11 +0000541 user["project_role_mappings"] = []
542 user["projects"] = []
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100543 projects = self.keystone.projects.list(user=user["_id"])
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100544 for project in projects:
tierno1546f2a2019-08-20 15:38:11 +0000545 user["projects"].append(project.name)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100546
garciadeblas4568a372021-03-24 09:19:48 +0100547 roles = self.keystone.roles.list(
548 user=user["_id"], project=project.id
549 )
tierno1546f2a2019-08-20 15:38:11 +0000550 for role in roles:
551 prm = {
552 "project": project.id,
553 "project_name": project.name,
554 "role_name": role.name,
555 "role": role.id,
556 }
557 user["project_role_mappings"].append(prm)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100558
559 return users
tierno4015b472019-06-10 13:57:29 +0000560 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000561 # self.logger.exception("Error during user listing using keystone: {}".format(e))
garciadeblas4568a372021-03-24 09:19:48 +0100562 raise AuthconnOperationException(
563 "Error during user listing using Keystone: {}".format(e)
564 )
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100565
tierno1f029d82019-06-13 22:37:04 +0000566 def get_role_list(self, filter_q=None):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100567 """
568 Get role list.
569
tierno1f029d82019-06-13 22:37:04 +0000570 :param filter_q: dictionary to filter role list by _id and/or name.
Eduardo Sousa37de0912019-05-23 02:17:22 +0100571 :return: returns the list of roles.
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100572 """
573 try:
tierno1f029d82019-06-13 22:37:04 +0000574 filter_name = None
575 if filter_q:
576 filter_name = filter_q.get("name")
577 roles_list = self.keystone.roles.list(name=filter_name)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100578
garciadeblas4568a372021-03-24 09:19:48 +0100579 roles = [
580 {
581 "name": role.name,
582 "_id": role.id,
583 "_admin": role.to_dict().get("_admin", {}),
584 "permissions": role.to_dict().get("permissions", {}),
585 }
586 for role in roles_list
587 if role.name != "service"
588 ]
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100589
tierno1f029d82019-06-13 22:37:04 +0000590 if filter_q and filter_q.get("_id"):
591 roles = [role for role in roles if filter_q["_id"] == role["_id"]]
592
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100593 return roles
tierno4015b472019-06-10 13:57:29 +0000594 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000595 # self.logger.exception("Error during user role listing using keystone: {}".format(e))
garciadeblas4568a372021-03-24 09:19:48 +0100596 raise AuthException(
597 "Error during user role listing using Keystone: {}".format(e),
598 http_code=HTTPStatus.UNAUTHORIZED,
599 )
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100600
delacruzramo01b15d32019-07-02 14:37:47 +0200601 def create_role(self, role_info):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100602 """
603 Create a role.
604
delacruzramo01b15d32019-07-02 14:37:47 +0200605 :param role_info: full role info.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100606 :raises AuthconnOperationException: if role creation failed.
607 """
608 try:
garciadeblas4568a372021-03-24 09:19:48 +0100609 result = self.keystone.roles.create(
610 role_info["name"],
611 permissions=role_info.get("permissions"),
612 _admin=role_info.get("_admin"),
613 )
tierno1f029d82019-06-13 22:37:04 +0000614 return result.id
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000615 except Conflict as ex:
tierno1f029d82019-06-13 22:37:04 +0000616 raise AuthconnConflictException(str(ex))
tierno4015b472019-06-10 13:57:29 +0000617 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000618 # self.logger.exception("Error during role creation using keystone: {}".format(e))
garciadeblas4568a372021-03-24 09:19:48 +0100619 raise AuthconnOperationException(
620 "Error during role creation using Keystone: {}".format(e)
621 )
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100622
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100623 def delete_role(self, role_id):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100624 """
625 Delete a role.
626
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100627 :param role_id: role identifier.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100628 :raises AuthconnOperationException: if role deletion failed.
629 """
630 try:
tierno1f029d82019-06-13 22:37:04 +0000631 result, detail = self.keystone.roles.delete(role_id)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100632
633 if result.status_code != 204:
tierno38dcfeb2019-06-10 16:44:00 +0000634 raise ClientException("error {} {}".format(result.status_code, detail))
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100635
636 return True
tierno4015b472019-06-10 13:57:29 +0000637 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000638 # self.logger.exception("Error during role deletion using keystone: {}".format(e))
garciadeblas4568a372021-03-24 09:19:48 +0100639 raise AuthconnOperationException(
640 "Error during role deletion using Keystone: {}".format(e)
641 )
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100642
delacruzramo01b15d32019-07-02 14:37:47 +0200643 def update_role(self, role_info):
tierno1f029d82019-06-13 22:37:04 +0000644 """
645 Change the name of a role
delacruzramo01b15d32019-07-02 14:37:47 +0200646 :param role_info: full role info
tierno1f029d82019-06-13 22:37:04 +0000647 :return: None
648 """
649 try:
delacruzramo01b15d32019-07-02 14:37:47 +0200650 rid = role_info["_id"]
garciadeblas4568a372021-03-24 09:19:48 +0100651 if not is_valid_uuid(rid): # Is this required?
delacruzramo01b15d32019-07-02 14:37:47 +0200652 role_obj_list = self.keystone.roles.list(name=rid)
tierno1f029d82019-06-13 22:37:04 +0000653 if not role_obj_list:
delacruzramo01b15d32019-07-02 14:37:47 +0200654 raise AuthconnNotFoundException("Role '{}' not found".format(rid))
655 rid = role_obj_list[0].id
garciadeblas4568a372021-03-24 09:19:48 +0100656 self.keystone.roles.update(
657 rid,
658 name=role_info["name"],
659 permissions=role_info.get("permissions"),
660 _admin=role_info.get("_admin"),
661 )
tierno1f029d82019-06-13 22:37:04 +0000662 except ClientException as e:
663 # self.logger.exception("Error during role update using keystone: {}".format(e))
garciadeblas4568a372021-03-24 09:19:48 +0100664 raise AuthconnOperationException(
665 "Error during role updating using Keystone: {}".format(e)
666 )
tierno1f029d82019-06-13 22:37:04 +0000667
tierno38dcfeb2019-06-10 16:44:00 +0000668 def get_project_list(self, filter_q=None):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100669 """
670 Get all the projects.
671
Eduardo Sousafa54cd92019-05-20 15:58:41 +0100672 :param filter_q: dictionary to filter project list.
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100673 :return: list of projects
674 """
675 try:
tiernoad6d5332020-02-19 14:29:49 +0000676 self._get_domains()
677 filter_name = filter_domain = None
tierno38dcfeb2019-06-10 16:44:00 +0000678 if filter_q:
679 filter_name = filter_q.get("name")
tiernoad6d5332020-02-19 14:29:49 +0000680 if filter_q.get("domain_name"):
681 filter_domain = self.domains_name2id.get(filter_q["domain_name"])
682 if filter_q.get("domain_id"):
683 filter_domain = filter_q["domain_id"]
684
garciadeblas4568a372021-03-24 09:19:48 +0100685 projects = self.keystone.projects.list(
686 name=filter_name, domain=filter_domain
687 )
tierno38dcfeb2019-06-10 16:44:00 +0000688
garciadeblas4568a372021-03-24 09:19:48 +0100689 projects = [
690 {
691 "name": project.name,
692 "_id": project.id,
693 "_admin": project.to_dict().get("_admin", {}), # TODO: REVISE
694 "quotas": project.to_dict().get("quotas", {}), # TODO: REVISE
695 "domain_name": self.domains_id2name.get(project.domain_id),
696 }
697 for project in projects
698 ]
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100699
tierno38dcfeb2019-06-10 16:44:00 +0000700 if filter_q and filter_q.get("_id"):
garciadeblas4568a372021-03-24 09:19:48 +0100701 projects = [
702 project for project in projects if filter_q["_id"] == project["_id"]
703 ]
Eduardo Sousafa54cd92019-05-20 15:58:41 +0100704
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100705 return projects
tierno4015b472019-06-10 13:57:29 +0000706 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000707 # self.logger.exception("Error during user project listing using keystone: {}".format(e))
garciadeblas4568a372021-03-24 09:19:48 +0100708 raise AuthException(
709 "Error during user project listing using Keystone: {}".format(e),
710 http_code=HTTPStatus.UNAUTHORIZED,
711 )
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100712
delacruzramo01b15d32019-07-02 14:37:47 +0200713 def create_project(self, project_info):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100714 """
715 Create a project.
716
delacruzramo01b15d32019-07-02 14:37:47 +0200717 :param project_info: full project info.
tierno4015b472019-06-10 13:57:29 +0000718 :return: the internal id of the created project
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100719 :raises AuthconnOperationException: if project creation failed.
720 """
721 try:
tierno6486f742020-02-13 16:30:14 +0000722 result = self.keystone.projects.create(
723 project_info["name"],
garciadeblas4568a372021-03-24 09:19:48 +0100724 domain=self._get_domain_id(
725 project_info.get("domain_name", self.project_domain_name_list[0])
726 ),
tierno6486f742020-02-13 16:30:14 +0000727 _admin=project_info["_admin"],
garciadeblas4568a372021-03-24 09:19:48 +0100728 quotas=project_info.get("quotas", {}),
tierno6486f742020-02-13 16:30:14 +0000729 )
tierno4015b472019-06-10 13:57:29 +0000730 return result.id
731 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000732 # self.logger.exception("Error during project creation using keystone: {}".format(e))
garciadeblas4568a372021-03-24 09:19:48 +0100733 raise AuthconnOperationException(
734 "Error during project creation using Keystone: {}".format(e)
735 )
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100736
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100737 def delete_project(self, project_id):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100738 """
739 Delete a project.
740
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100741 :param project_id: project identifier.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100742 :raises AuthconnOperationException: if project deletion failed.
743 """
744 try:
tierno38dcfeb2019-06-10 16:44:00 +0000745 # projects = self.keystone.projects.list()
746 # project_obj = [project for project in projects if project.id == project_id][0]
747 # result, _ = self.keystone.projects.delete(project_obj)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100748
tierno38dcfeb2019-06-10 16:44:00 +0000749 result, detail = self.keystone.projects.delete(project_id)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100750 if result.status_code != 204:
tierno38dcfeb2019-06-10 16:44:00 +0000751 raise ClientException("error {} {}".format(result.status_code, detail))
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100752
753 return True
tierno4015b472019-06-10 13:57:29 +0000754 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000755 # self.logger.exception("Error during project deletion using keystone: {}".format(e))
garciadeblas4568a372021-03-24 09:19:48 +0100756 raise AuthconnOperationException(
757 "Error during project deletion using Keystone: {}".format(e)
758 )
tierno4015b472019-06-10 13:57:29 +0000759
delacruzramo01b15d32019-07-02 14:37:47 +0200760 def update_project(self, project_id, project_info):
tierno4015b472019-06-10 13:57:29 +0000761 """
762 Change the name of a project
763 :param project_id: project to be changed
delacruzramo01b15d32019-07-02 14:37:47 +0200764 :param project_info: full project info
tierno4015b472019-06-10 13:57:29 +0000765 :return: None
766 """
767 try:
garciadeblas4568a372021-03-24 09:19:48 +0100768 self.keystone.projects.update(
769 project_id,
770 name=project_info["name"],
771 _admin=project_info["_admin"],
772 quotas=project_info.get("quotas", {}),
773 )
tierno4015b472019-06-10 13:57:29 +0000774 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000775 # self.logger.exception("Error during project update using keystone: {}".format(e))
garciadeblas4568a372021-03-24 09:19:48 +0100776 raise AuthconnOperationException(
777 "Error during project update using Keystone: {}".format(e)
778 )
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100779
tiernoad6d5332020-02-19 14:29:49 +0000780 def assign_role_to_user(self, user_obj, project, role):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100781 """
782 Assigning a role to a user in a project.
783
tiernoad6d5332020-02-19 14:29:49 +0000784 :param user_obj: user object, obtained with keystone.users.get or list.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100785 :param project: project name.
786 :param role: role name.
787 :raises AuthconnOperationException: if role assignment failed.
788 """
789 try:
tiernoad6d5332020-02-19 14:29:49 +0000790 try:
Eduardo Sousa44603902019-06-04 08:10:32 +0100791 project_obj = self.keystone.projects.get(project)
tiernoad6d5332020-02-19 14:29:49 +0000792 except Exception:
tiernocf042d32019-06-13 09:06:40 +0000793 project_obj_list = self.keystone.projects.list(name=project)
794 if not project_obj_list:
garciadeblas4568a372021-03-24 09:19:48 +0100795 raise AuthconnNotFoundException(
796 "Project '{}' not found".format(project)
797 )
tiernocf042d32019-06-13 09:06:40 +0000798 project_obj = project_obj_list[0]
Eduardo Sousa44603902019-06-04 08:10:32 +0100799
tiernoad6d5332020-02-19 14:29:49 +0000800 try:
Eduardo Sousa44603902019-06-04 08:10:32 +0100801 role_obj = self.keystone.roles.get(role)
tiernoad6d5332020-02-19 14:29:49 +0000802 except Exception:
tiernocf042d32019-06-13 09:06:40 +0000803 role_obj_list = self.keystone.roles.list(name=role)
804 if not role_obj_list:
805 raise AuthconnNotFoundException("Role '{}' not found".format(role))
806 role_obj = role_obj_list[0]
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100807
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000808 self.keystone.roles.grant(role_obj, user=user_obj, project=project_obj)
tierno4015b472019-06-10 13:57:29 +0000809 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000810 # self.logger.exception("Error during user role assignment using keystone: {}".format(e))
garciadeblas4568a372021-03-24 09:19:48 +0100811 raise AuthconnOperationException(
812 "Error during role '{}' assignment to user '{}' and project '{}' using "
813 "Keystone: {}".format(role, user_obj.name, project, e)
814 )
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100815
tiernoad6d5332020-02-19 14:29:49 +0000816 def remove_role_from_user(self, user_obj, project, role):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100817 """
818 Remove a role from a user in a project.
819
tiernoad6d5332020-02-19 14:29:49 +0000820 :param user_obj: user object, obtained with keystone.users.get or list.
tiernocf042d32019-06-13 09:06:40 +0000821 :param project: project name or id.
822 :param role: role name or id.
823
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100824 :raises AuthconnOperationException: if role assignment revocation failed.
825 """
826 try:
tiernoad6d5332020-02-19 14:29:49 +0000827 try:
tiernocf042d32019-06-13 09:06:40 +0000828 project_obj = self.keystone.projects.get(project)
tiernoad6d5332020-02-19 14:29:49 +0000829 except Exception:
tiernocf042d32019-06-13 09:06:40 +0000830 project_obj_list = self.keystone.projects.list(name=project)
831 if not project_obj_list:
garciadeblas4568a372021-03-24 09:19:48 +0100832 raise AuthconnNotFoundException(
833 "Project '{}' not found".format(project)
834 )
tiernocf042d32019-06-13 09:06:40 +0000835 project_obj = project_obj_list[0]
836
tiernoad6d5332020-02-19 14:29:49 +0000837 try:
tiernocf042d32019-06-13 09:06:40 +0000838 role_obj = self.keystone.roles.get(role)
tiernoad6d5332020-02-19 14:29:49 +0000839 except Exception:
tiernocf042d32019-06-13 09:06:40 +0000840 role_obj_list = self.keystone.roles.list(name=role)
841 if not role_obj_list:
842 raise AuthconnNotFoundException("Role '{}' not found".format(role))
843 role_obj = role_obj_list[0]
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100844
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000845 self.keystone.roles.revoke(role_obj, user=user_obj, project=project_obj)
tierno4015b472019-06-10 13:57:29 +0000846 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000847 # self.logger.exception("Error during user role revocation using keystone: {}".format(e))
garciadeblas4568a372021-03-24 09:19:48 +0100848 raise AuthconnOperationException(
849 "Error during role '{}' revocation to user '{}' and project '{}' using "
850 "Keystone: {}".format(role, user_obj.name, project, e)
851 )