blob: d71408a3912677aa513fafa0ad931e355bcf8ea5 [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
tierno23acf402019-08-28 13:36:34 +000044from osm_nbi.validation import is_valid_uuid
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
55 self.auth_url = "http://{0}:{1}/v3".format(config.get("auth_url", "keystone"), config.get("auth_port", "5000"))
tierno6486f742020-02-13 16:30:14 +000056 self.user_domain_name_list = config.get("user_domain_name", "default")
57 self.user_domain_name_list = self.user_domain_name_list.split(",")
tiernoad6d5332020-02-19 14:29:49 +000058 # read only domain list
59 self.user_domain_ro_list = [x[:-3] for x in self.user_domain_name_list if x.endswith(":ro")]
60 # remove the ":ro"
61 self.user_domain_name_list = [x if not x.endswith(":ro") else x[:-3] for x in self.user_domain_name_list]
62
Eduardo Sousa819d34c2018-07-31 01:20:02 +010063 self.admin_project = config.get("service_project", "service")
64 self.admin_username = config.get("service_username", "nbi")
65 self.admin_password = config.get("service_password", "nbi")
tierno6486f742020-02-13 16:30:14 +000066 self.project_domain_name_list = config.get("project_domain_name", "default")
67 self.project_domain_name_list = self.project_domain_name_list.split(",")
68 if len(self.user_domain_name_list) != len(self.project_domain_name_list):
69 raise ValueError("Invalid configuration parameter fo authenticate. 'project_domain_name' and "
70 "'user_domain_name' must be a comma-separated list with the same size. Revise "
71 "configuration or/and 'OSMNBI_AUTHENTICATION_PROJECT_DOMAIN_NAME', "
72 "'OSMNBI_AUTHENTICATION_USER_DOMAIN_NAME' Variables")
Eduardo Sousa819d34c2018-07-31 01:20:02 +010073
Eduardo Sousa29933fc2018-11-14 06:36:35 +000074 # Waiting for Keystone to be up
75 available = None
76 counter = 300
77 while available is None:
78 time.sleep(1)
79 try:
80 result = requests.get(self.auth_url)
81 available = True if result.status_code == 200 else None
82 except Exception:
83 counter -= 1
84 if counter == 0:
85 raise AuthException("Keystone not available after 300s timeout")
86
tierno6486f742020-02-13 16:30:14 +000087 self.auth = v3.Password(user_domain_name=self.user_domain_name_list[0],
Eduardo Sousa819d34c2018-07-31 01:20:02 +010088 username=self.admin_username,
89 password=self.admin_password,
tierno6486f742020-02-13 16:30:14 +000090 project_domain_name=self.project_domain_name_list[0],
Eduardo Sousa819d34c2018-07-31 01:20:02 +010091 project_name=self.admin_project,
92 auth_url=self.auth_url)
93 self.sess = session.Session(auth=self.auth)
94 self.keystone = client.Client(session=self.sess)
95
tierno6486f742020-02-13 16:30:14 +000096 def authenticate(self, credentials, token_info=None):
Eduardo Sousa819d34c2018-07-31 01:20:02 +010097 """
tierno701018c2019-06-25 11:13:14 +000098 Authenticate a user using username/password or token_info, plus project
tierno6486f742020-02-13 16:30:14 +000099 :param credentials: dictionary that contains:
100 username: name, id or None
101 password: password or None
102 project_id: name, id, or None. If None first found project will be used to get an scope token
103 project_domain_name: (Optional) To use a concrete domain for the project
104 user_domain_name: (Optional) To use a concrete domain for the project
105 other items are allowed and ignored
tierno701018c2019-06-25 11:13:14 +0000106 :param token_info: previous token_info to obtain authorization
tierno38dcfeb2019-06-10 16:44:00 +0000107 :return: the scoped token info or raises an exception. The token is a dictionary with:
108 _id: token string id,
109 username: username,
110 project_id: scoped_token project_id,
111 project_name: scoped_token project_name,
112 expires: epoch time when it expires,
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100113
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100114 """
tierno6486f742020-02-13 16:30:14 +0000115 username = None
116 user_id = None
117 project_id = None
118 project_name = None
119 if credentials.get("project_domain_name"):
120 project_domain_name_list = (credentials["project_domain_name"], )
121 else:
122 project_domain_name_list = self.project_domain_name_list
123 if credentials.get("user_domain_name"):
124 user_domain_name_list = (credentials["user_domain_name"], )
125 else:
126 user_domain_name_list = self.user_domain_name_list
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100127
tierno6486f742020-02-13 16:30:14 +0000128 for index, project_domain_name in enumerate(project_domain_name_list):
129 user_domain_name = user_domain_name_list[index]
130 try:
131 if credentials.get("username"):
132 if is_valid_uuid(credentials["username"]):
133 user_id = credentials["username"]
134 else:
135 username = credentials["username"]
136
137 # get an unscoped token firstly
138 unscoped_token = self.keystone.get_raw_token_from_identity_service(
139 auth_url=self.auth_url,
140 user_id=user_id,
141 username=username,
142 password=credentials.get("password"),
143 user_domain_name=user_domain_name,
144 project_domain_name=project_domain_name)
145 elif token_info:
146 unscoped_token = self.keystone.tokens.validate(token=token_info.get("_id"))
tierno38dcfeb2019-06-10 16:44:00 +0000147 else:
tierno6486f742020-02-13 16:30:14 +0000148 raise AuthException("Provide credentials: username/password or Authorization Bearer token",
149 http_code=HTTPStatus.UNAUTHORIZED)
tierno38dcfeb2019-06-10 16:44:00 +0000150
tierno6486f742020-02-13 16:30:14 +0000151 if not credentials.get("project_id"):
152 # get first project for the user
153 project_list = self.keystone.projects.list(user=unscoped_token["user"]["id"])
154 if not project_list:
155 raise AuthException("The user {} has not any project and cannot be used for authentication".
156 format(credentials.get("username")), http_code=HTTPStatus.UNAUTHORIZED)
157 project_id = project_list[0].id
158 else:
159 if is_valid_uuid(credentials["project_id"]):
160 project_id = credentials["project_id"]
161 else:
162 project_name = credentials["project_id"]
163
164 scoped_token = self.keystone.get_raw_token_from_identity_service(
tierno38dcfeb2019-06-10 16:44:00 +0000165 auth_url=self.auth_url,
tierno6486f742020-02-13 16:30:14 +0000166 project_name=project_name,
167 project_id=project_id,
168 user_domain_name=user_domain_name,
169 project_domain_name=project_domain_name,
170 token=unscoped_token["auth_token"])
tierno38dcfeb2019-06-10 16:44:00 +0000171
tierno6486f742020-02-13 16:30:14 +0000172 auth_token = {
173 "_id": scoped_token.auth_token,
174 "id": scoped_token.auth_token,
175 "user_id": scoped_token.user_id,
176 "username": scoped_token.username,
177 "project_id": scoped_token.project_id,
178 "project_name": scoped_token.project_name,
179 "project_domain_name": scoped_token.project_domain_name,
180 "user_domain_name": scoped_token.user_domain_name,
181 "expires": scoped_token.expires.timestamp(),
182 "issued_at": scoped_token.issued.timestamp()
183 }
tierno38dcfeb2019-06-10 16:44:00 +0000184
tierno6486f742020-02-13 16:30:14 +0000185 return auth_token
186 except ClientException as e:
187 if index >= len(user_domain_name_list)-1 or index >= len(project_domain_name_list)-1:
188 # if last try, launch exception
189 # self.logger.exception("Error during user authentication using keystone: {}".format(e))
190 raise AuthException("Error during user authentication using Keystone: {}".format(e),
191 http_code=HTTPStatus.UNAUTHORIZED)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100192
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100193 def validate_token(self, token):
194 """
195 Check if the token is valid.
196
tierno701018c2019-06-25 11:13:14 +0000197 :param token: token id to be validated
tiernoa6bb45d2019-06-14 09:45:39 +0000198 :return: dictionary with information associated with the token:
199 "expires":
200 "_id": token_id,
201 "project_id": project_id,
202 "username": ,
203 "roles": list with dict containing {name, id}
204 If the token is not valid an exception is raised.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100205 """
206 if not token:
207 return
208
209 try:
210 token_info = self.keystone.tokens.validate(token=token)
tiernoa6bb45d2019-06-14 09:45:39 +0000211 ses = {
212 "_id": token_info["auth_token"],
tierno701018c2019-06-25 11:13:14 +0000213 "id": token_info["auth_token"],
tiernoa6bb45d2019-06-14 09:45:39 +0000214 "project_id": token_info["project"]["id"],
215 "project_name": token_info["project"]["name"],
216 "user_id": token_info["user"]["id"],
217 "username": token_info["user"]["name"],
218 "roles": token_info["roles"],
tierno701018c2019-06-25 11:13:14 +0000219 "expires": token_info.expires.timestamp(),
220 "issued_at": token_info.issued.timestamp()
tiernoa6bb45d2019-06-14 09:45:39 +0000221 }
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100222
tiernoa6bb45d2019-06-14 09:45:39 +0000223 return ses
tierno4015b472019-06-10 13:57:29 +0000224 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000225 # self.logger.exception("Error during token validation using keystone: {}".format(e))
tierno4015b472019-06-10 13:57:29 +0000226 raise AuthException("Error during token validation using Keystone: {}".format(e),
227 http_code=HTTPStatus.UNAUTHORIZED)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100228
229 def revoke_token(self, token):
230 """
231 Invalidate a token.
232
233 :param token: token to be revoked
234 """
235 try:
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000236 self.logger.info("Revoking token: " + token)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100237 self.keystone.tokens.revoke_token(token=token)
238
239 return True
tierno4015b472019-06-10 13:57:29 +0000240 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000241 # self.logger.exception("Error during token revocation using keystone: {}".format(e))
tierno4015b472019-06-10 13:57:29 +0000242 raise AuthException("Error during token revocation using Keystone: {}".format(e),
243 http_code=HTTPStatus.UNAUTHORIZED)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100244
tiernoad6d5332020-02-19 14:29:49 +0000245 def _get_domain_id(self, domain_name, fail_if_not_found=True):
246 """
247 Get the domain id from the domain_name
248 :param domain_name: Can be the name or id
249 :param fail_if_not_found: If False it returns None instead of raising an exception if not found
250 :return: str or None/exception if domain is not found
251 """
252 domain_id = self.domains_name2id.get(domain_name)
253 if not domain_id:
254 self._get_domains()
255 domain_id = self.domains_name2id.get(domain_name)
256 if not domain_id and domain_name in self.domains_id2name:
257 # domain_name is already an id
258 return domain_name
259 if not domain_id and fail_if_not_found:
260 raise AuthconnNotFoundException("Domain {} cannot be found".format(domain_name))
261 return domain_id
262
263 def _get_domains(self):
264 """
265 Obtain a dictionary with domain_id to domain_name, stored at self.domains_id2name
266 and from domain_name to domain_id, sored at self.domains_name2id
267 :return: None. Exceptions are ignored
268 """
269 try:
270 domains = self.keystone.domains.list()
271 self.domains_id2name = {x.id: x.name for x in domains}
272 self.domains_name2id = {x.name: x.id for x in domains}
273 except Exception:
274 pass
275
delacruzramo01b15d32019-07-02 14:37:47 +0200276 def create_user(self, user_info):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100277 """
278 Create a user.
279
delacruzramo01b15d32019-07-02 14:37:47 +0200280 :param user_info: full user info.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100281 :raises AuthconnOperationException: if user creation failed.
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100282 :return: returns the id of the user in keystone.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100283 """
284 try:
tiernoad6d5332020-02-19 14:29:49 +0000285
286 if user_info.get("domain_name") and user_info["domain_name"] in self.user_domain_ro_list:
287 raise AuthconnConflictException("Cannot create a user in the read only domain {}".
288 format(user_info["domain_name"]))
289
tierno6486f742020-02-13 16:30:14 +0000290 new_user = self.keystone.users.create(
291 user_info["username"], password=user_info["password"],
tiernoad6d5332020-02-19 14:29:49 +0000292 domain=self._get_domain_id(user_info.get("domain_name", self.user_domain_name_list[0])),
tierno6486f742020-02-13 16:30:14 +0000293 _admin=user_info["_admin"])
delacruzramo01b15d32019-07-02 14:37:47 +0200294 if "project_role_mappings" in user_info.keys():
295 for mapping in user_info["project_role_mappings"]:
tiernoad6d5332020-02-19 14:29:49 +0000296 self.assign_role_to_user(new_user, mapping["project"], mapping["role"])
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100297 return {"username": new_user.name, "_id": new_user.id}
tiernocf042d32019-06-13 09:06:40 +0000298 except Conflict as e:
299 # self.logger.exception("Error during user creation using keystone: {}".format(e))
300 raise AuthconnOperationException(e, http_code=HTTPStatus.CONFLICT)
tierno4015b472019-06-10 13:57:29 +0000301 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000302 # self.logger.exception("Error during user creation using keystone: {}".format(e))
tierno4015b472019-06-10 13:57:29 +0000303 raise AuthconnOperationException("Error during user creation using Keystone: {}".format(e))
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100304
delacruzramo01b15d32019-07-02 14:37:47 +0200305 def update_user(self, user_info):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100306 """
tiernocf042d32019-06-13 09:06:40 +0000307 Change the user name and/or password.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100308
delacruzramo01b15d32019-07-02 14:37:47 +0200309 :param user_info: user info modifications
tiernocf042d32019-06-13 09:06:40 +0000310 :raises AuthconnOperationException: if change failed.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100311 """
312 try:
delacruzramo01b15d32019-07-02 14:37:47 +0200313 user = user_info.get("_id") or user_info.get("username")
tiernoad6d5332020-02-19 14:29:49 +0000314 try:
315 user_obj = self.keystone.users.get(user)
316 except Exception:
317 user_obj = None
318 if not user_obj:
319 for user_domain in self.user_domain_name_list:
320 domain_id = self._get_domain_id(user_domain, fail_if_not_found=False)
321 if not domain_id:
322 continue
323 user_obj_list = self.keystone.users.list(name=user, domain=domain_id)
324 if user_obj_list:
325 user_obj = user_obj_list[0]
326 break
327 else: # user not found
328 raise AuthconnNotFoundException("User '{}' not found".format(user))
329
delacruzramo01b15d32019-07-02 14:37:47 +0200330 user_id = user_obj.id
tiernoad6d5332020-02-19 14:29:49 +0000331 domain_id = user_obj.domain_id
332 domain_name = self.domains_id2name.get(domain_id)
333
334 if domain_name in self.user_domain_ro_list:
335 if user_info.get("password") or user_info.get("username"):
336 raise AuthconnConflictException("Cannot update the user {} belonging to a read only domain {}".
337 format(user, domain_name))
338
339 elif user_info.get("password") or user_info.get("username") \
delacruzramo01b15d32019-07-02 14:37:47 +0200340 or user_info.get("add_project_role_mappings") or user_info.get("remove_project_role_mappings"):
tiernoad6d5332020-02-19 14:29:49 +0000341 # if user_index>0, it is an external domain, that should not be updated
delacruzramo15ec7062019-12-26 10:09:04 +0000342 ctime = user_obj._admin.get("created", 0) if hasattr(user_obj, "_admin") else 0
delacruzramo01b15d32019-07-02 14:37:47 +0200343 self.keystone.users.update(user_id, password=user_info.get("password"), name=user_info.get("username"),
delacruzramo15ec7062019-12-26 10:09:04 +0000344 _admin={"created": ctime, "modified": time.time()})
tiernoad6d5332020-02-19 14:29:49 +0000345
delacruzramo01b15d32019-07-02 14:37:47 +0200346 for mapping in user_info.get("remove_project_role_mappings", []):
tiernoad6d5332020-02-19 14:29:49 +0000347 self.remove_role_from_user(user_obj, mapping["project"], mapping["role"])
delacruzramo01b15d32019-07-02 14:37:47 +0200348 for mapping in user_info.get("add_project_role_mappings", []):
tiernoad6d5332020-02-19 14:29:49 +0000349 self.assign_role_to_user(user_obj, mapping["project"], mapping["role"])
tierno4015b472019-06-10 13:57:29 +0000350 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000351 # self.logger.exception("Error during user password/name update using keystone: {}".format(e))
delacruzramo01b15d32019-07-02 14:37:47 +0200352 raise AuthconnOperationException("Error during user update using Keystone: {}".format(e))
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100353
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100354 def delete_user(self, user_id):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100355 """
356 Delete user.
357
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100358 :param user_id: user identifier.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100359 :raises AuthconnOperationException: if user deletion failed.
360 """
361 try:
tiernoad6d5332020-02-19 14:29:49 +0000362 user_obj = self.keystone.users.get(user_id)
363 domain_id = user_obj.domain_id
364 domain_name = self.domains_id2name.get(domain_id)
365 if domain_name in self.user_domain_ro_list:
366 raise AuthconnConflictException("Cannot delete user {} belonging to a read only domain {}".
367 format(user_id, domain_name))
368
tierno38dcfeb2019-06-10 16:44:00 +0000369 result, detail = self.keystone.users.delete(user_id)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100370 if result.status_code != 204:
tierno38dcfeb2019-06-10 16:44:00 +0000371 raise ClientException("error {} {}".format(result.status_code, detail))
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100372 return True
tierno4015b472019-06-10 13:57:29 +0000373 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000374 # self.logger.exception("Error during user deletion using keystone: {}".format(e))
tierno4015b472019-06-10 13:57:29 +0000375 raise AuthconnOperationException("Error during user deletion using Keystone: {}".format(e))
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100376
tiernocf042d32019-06-13 09:06:40 +0000377 def get_user_list(self, filter_q=None):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100378 """
379 Get user list.
380
tiernoad6d5332020-02-19 14:29:49 +0000381 :param filter_q: dictionary to filter user list by one or several
382 _id:
383 name (username is also admitted). If a user id is equal to the filter name, it is also provided
384 domain_id, domain_name
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100385 :return: returns a list of users.
386 """
387 try:
tiernoad6d5332020-02-19 14:29:49 +0000388 self._get_domains()
389 filter_name = filter_domain = None
tiernocf042d32019-06-13 09:06:40 +0000390 if filter_q:
391 filter_name = filter_q.get("name") or filter_q.get("username")
tiernoad6d5332020-02-19 14:29:49 +0000392 if filter_q.get("domain_name"):
393 filter_domain = self._get_domain_id(filter_q["domain_name"], fail_if_not_found=False)
394 # If domain is not found, use the same name to obtain an empty list
395 filter_domain = filter_domain or filter_q["domain_name"]
396 if filter_q.get("domain_id"):
397 filter_domain = filter_q["domain_id"]
398
399 users = self.keystone.users.list(name=filter_name, domain=filter_domain)
400 # get users from user_domain_name_list[1:], because it will not be provided in case of LDAP
401 if filter_domain is None and len(self.user_domain_name_list) > 1:
402 for user_domain in self.user_domain_name_list[1:]:
403 domain_id = self._get_domain_id(user_domain, fail_if_not_found=False)
404 if not domain_id:
405 continue
406 # find if users of this domain are already provided. In this case ignore
407 for u in users:
408 if u.domain_id == domain_id:
409 break
410 else:
411 users += self.keystone.users.list(name=filter_name, domain=domain_id)
412
413 # if filter name matches a user id, provide it also
414 if filter_name:
415 try:
416 user_obj = self.keystone.users.get(filter_name)
417 if user_obj not in users:
418 users.append(user_obj)
419 except Exception:
420 pass
421
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100422 users = [{
423 "username": user.name,
Eduardo Sousa203bad82019-05-23 01:41:18 +0100424 "_id": user.id,
delacruzramo01b15d32019-07-02 14:37:47 +0200425 "id": user.id,
tiernoad6d5332020-02-19 14:29:49 +0000426 "_admin": user.to_dict().get("_admin", {}), # TODO: REVISE
427 "domain_name": self.domains_id2name.get(user.domain_id)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100428 } for user in users if user.name != self.admin_username]
429
tiernocf042d32019-06-13 09:06:40 +0000430 if filter_q and filter_q.get("_id"):
431 users = [user for user in users if filter_q["_id"] == user["_id"]]
Eduardo Sousa2d5a5152019-05-20 15:41:54 +0100432
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100433 for user in users:
tierno1546f2a2019-08-20 15:38:11 +0000434 user["project_role_mappings"] = []
435 user["projects"] = []
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100436 projects = self.keystone.projects.list(user=user["_id"])
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100437 for project in projects:
tierno1546f2a2019-08-20 15:38:11 +0000438 user["projects"].append(project.name)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100439
tierno1546f2a2019-08-20 15:38:11 +0000440 roles = self.keystone.roles.list(user=user["_id"], project=project.id)
441 for role in roles:
442 prm = {
443 "project": project.id,
444 "project_name": project.name,
445 "role_name": role.name,
446 "role": role.id,
447 }
448 user["project_role_mappings"].append(prm)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100449
450 return users
tierno4015b472019-06-10 13:57:29 +0000451 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000452 # self.logger.exception("Error during user listing using keystone: {}".format(e))
tierno4015b472019-06-10 13:57:29 +0000453 raise AuthconnOperationException("Error during user listing using Keystone: {}".format(e))
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100454
tierno1f029d82019-06-13 22:37:04 +0000455 def get_role_list(self, filter_q=None):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100456 """
457 Get role list.
458
tierno1f029d82019-06-13 22:37:04 +0000459 :param filter_q: dictionary to filter role list by _id and/or name.
Eduardo Sousa37de0912019-05-23 02:17:22 +0100460 :return: returns the list of roles.
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100461 """
462 try:
tierno1f029d82019-06-13 22:37:04 +0000463 filter_name = None
464 if filter_q:
465 filter_name = filter_q.get("name")
466 roles_list = self.keystone.roles.list(name=filter_name)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100467
468 roles = [{
469 "name": role.name,
delacruzramo01b15d32019-07-02 14:37:47 +0200470 "_id": role.id,
471 "_admin": role.to_dict().get("_admin", {}),
472 "permissions": role.to_dict().get("permissions", {})
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100473 } for role in roles_list if role.name != "service"]
474
tierno1f029d82019-06-13 22:37:04 +0000475 if filter_q and filter_q.get("_id"):
476 roles = [role for role in roles if filter_q["_id"] == role["_id"]]
477
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100478 return roles
tierno4015b472019-06-10 13:57:29 +0000479 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000480 # self.logger.exception("Error during user role listing using keystone: {}".format(e))
tierno4015b472019-06-10 13:57:29 +0000481 raise AuthException("Error during user role listing using Keystone: {}".format(e),
482 http_code=HTTPStatus.UNAUTHORIZED)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100483
delacruzramo01b15d32019-07-02 14:37:47 +0200484 def create_role(self, role_info):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100485 """
486 Create a role.
487
delacruzramo01b15d32019-07-02 14:37:47 +0200488 :param role_info: full role info.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100489 :raises AuthconnOperationException: if role creation failed.
490 """
491 try:
delacruzramo01b15d32019-07-02 14:37:47 +0200492 result = self.keystone.roles.create(role_info["name"], permissions=role_info.get("permissions"),
493 _admin=role_info.get("_admin"))
tierno1f029d82019-06-13 22:37:04 +0000494 return result.id
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000495 except Conflict as ex:
tierno1f029d82019-06-13 22:37:04 +0000496 raise AuthconnConflictException(str(ex))
tierno4015b472019-06-10 13:57:29 +0000497 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000498 # self.logger.exception("Error during role creation using keystone: {}".format(e))
tierno4015b472019-06-10 13:57:29 +0000499 raise AuthconnOperationException("Error during role creation using Keystone: {}".format(e))
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100500
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100501 def delete_role(self, role_id):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100502 """
503 Delete a role.
504
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100505 :param role_id: role identifier.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100506 :raises AuthconnOperationException: if role deletion failed.
507 """
508 try:
tierno1f029d82019-06-13 22:37:04 +0000509 result, detail = self.keystone.roles.delete(role_id)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100510
511 if result.status_code != 204:
tierno38dcfeb2019-06-10 16:44:00 +0000512 raise ClientException("error {} {}".format(result.status_code, detail))
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100513
514 return True
tierno4015b472019-06-10 13:57:29 +0000515 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000516 # self.logger.exception("Error during role deletion using keystone: {}".format(e))
tierno4015b472019-06-10 13:57:29 +0000517 raise AuthconnOperationException("Error during role deletion using Keystone: {}".format(e))
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100518
delacruzramo01b15d32019-07-02 14:37:47 +0200519 def update_role(self, role_info):
tierno1f029d82019-06-13 22:37:04 +0000520 """
521 Change the name of a role
delacruzramo01b15d32019-07-02 14:37:47 +0200522 :param role_info: full role info
tierno1f029d82019-06-13 22:37:04 +0000523 :return: None
524 """
525 try:
delacruzramo01b15d32019-07-02 14:37:47 +0200526 rid = role_info["_id"]
527 if not is_valid_uuid(rid): # Is this required?
528 role_obj_list = self.keystone.roles.list(name=rid)
tierno1f029d82019-06-13 22:37:04 +0000529 if not role_obj_list:
delacruzramo01b15d32019-07-02 14:37:47 +0200530 raise AuthconnNotFoundException("Role '{}' not found".format(rid))
531 rid = role_obj_list[0].id
532 self.keystone.roles.update(rid, name=role_info["name"], permissions=role_info.get("permissions"),
533 _admin=role_info.get("_admin"))
tierno1f029d82019-06-13 22:37:04 +0000534 except ClientException as e:
535 # self.logger.exception("Error during role update using keystone: {}".format(e))
536 raise AuthconnOperationException("Error during role updating using Keystone: {}".format(e))
537
tierno38dcfeb2019-06-10 16:44:00 +0000538 def get_project_list(self, filter_q=None):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100539 """
540 Get all the projects.
541
Eduardo Sousafa54cd92019-05-20 15:58:41 +0100542 :param filter_q: dictionary to filter project list.
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100543 :return: list of projects
544 """
545 try:
tiernoad6d5332020-02-19 14:29:49 +0000546 self._get_domains()
547 filter_name = filter_domain = None
tierno38dcfeb2019-06-10 16:44:00 +0000548 if filter_q:
549 filter_name = filter_q.get("name")
tiernoad6d5332020-02-19 14:29:49 +0000550 if filter_q.get("domain_name"):
551 filter_domain = self.domains_name2id.get(filter_q["domain_name"])
552 if filter_q.get("domain_id"):
553 filter_domain = filter_q["domain_id"]
554
555 projects = self.keystone.projects.list(name=filter_name, domain=filter_domain)
tierno38dcfeb2019-06-10 16:44:00 +0000556
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100557 projects = [{
558 "name": project.name,
delacruzramo01b15d32019-07-02 14:37:47 +0200559 "_id": project.id,
delacruzramo32bab472019-09-13 12:24:22 +0200560 "_admin": project.to_dict().get("_admin", {}), # TODO: REVISE
561 "quotas": project.to_dict().get("quotas", {}), # TODO: REVISE
tiernoad6d5332020-02-19 14:29:49 +0000562 "domain_name": self.domains_id2name.get(project.domain_id)
tierno38dcfeb2019-06-10 16:44:00 +0000563 } for project in projects]
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100564
tierno38dcfeb2019-06-10 16:44:00 +0000565 if filter_q and filter_q.get("_id"):
Eduardo Sousafa54cd92019-05-20 15:58:41 +0100566 projects = [project for project in projects
tierno38dcfeb2019-06-10 16:44:00 +0000567 if filter_q["_id"] == project["_id"]]
Eduardo Sousafa54cd92019-05-20 15:58:41 +0100568
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100569 return projects
tierno4015b472019-06-10 13:57:29 +0000570 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000571 # self.logger.exception("Error during user project listing using keystone: {}".format(e))
tierno4015b472019-06-10 13:57:29 +0000572 raise AuthException("Error during user project listing using Keystone: {}".format(e),
573 http_code=HTTPStatus.UNAUTHORIZED)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100574
delacruzramo01b15d32019-07-02 14:37:47 +0200575 def create_project(self, project_info):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100576 """
577 Create a project.
578
delacruzramo01b15d32019-07-02 14:37:47 +0200579 :param project_info: full project info.
tierno4015b472019-06-10 13:57:29 +0000580 :return: the internal id of the created project
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100581 :raises AuthconnOperationException: if project creation failed.
582 """
583 try:
tierno6486f742020-02-13 16:30:14 +0000584 result = self.keystone.projects.create(
585 project_info["name"],
tiernoad6d5332020-02-19 14:29:49 +0000586 domain=self._get_domain_id(project_info.get("domain_name", self.project_domain_name_list[0])),
tierno6486f742020-02-13 16:30:14 +0000587 _admin=project_info["_admin"],
588 quotas=project_info.get("quotas", {})
589 )
tierno4015b472019-06-10 13:57:29 +0000590 return result.id
591 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000592 # self.logger.exception("Error during project creation using keystone: {}".format(e))
tierno4015b472019-06-10 13:57:29 +0000593 raise AuthconnOperationException("Error during project creation using Keystone: {}".format(e))
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100594
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100595 def delete_project(self, project_id):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100596 """
597 Delete a project.
598
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100599 :param project_id: project identifier.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100600 :raises AuthconnOperationException: if project deletion failed.
601 """
602 try:
tierno38dcfeb2019-06-10 16:44:00 +0000603 # projects = self.keystone.projects.list()
604 # project_obj = [project for project in projects if project.id == project_id][0]
605 # result, _ = self.keystone.projects.delete(project_obj)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100606
tierno38dcfeb2019-06-10 16:44:00 +0000607 result, detail = self.keystone.projects.delete(project_id)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100608 if result.status_code != 204:
tierno38dcfeb2019-06-10 16:44:00 +0000609 raise ClientException("error {} {}".format(result.status_code, detail))
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100610
611 return True
tierno4015b472019-06-10 13:57:29 +0000612 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000613 # self.logger.exception("Error during project deletion using keystone: {}".format(e))
tierno4015b472019-06-10 13:57:29 +0000614 raise AuthconnOperationException("Error during project deletion using Keystone: {}".format(e))
615
delacruzramo01b15d32019-07-02 14:37:47 +0200616 def update_project(self, project_id, project_info):
tierno4015b472019-06-10 13:57:29 +0000617 """
618 Change the name of a project
619 :param project_id: project to be changed
delacruzramo01b15d32019-07-02 14:37:47 +0200620 :param project_info: full project info
tierno4015b472019-06-10 13:57:29 +0000621 :return: None
622 """
623 try:
delacruzramo32bab472019-09-13 12:24:22 +0200624 self.keystone.projects.update(project_id, name=project_info["name"],
625 _admin=project_info["_admin"],
626 quotas=project_info.get("quotas", {})
627 )
tierno4015b472019-06-10 13:57:29 +0000628 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000629 # self.logger.exception("Error during project update using keystone: {}".format(e))
delacruzramo01b15d32019-07-02 14:37:47 +0200630 raise AuthconnOperationException("Error during project update using Keystone: {}".format(e))
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100631
tiernoad6d5332020-02-19 14:29:49 +0000632 def assign_role_to_user(self, user_obj, project, role):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100633 """
634 Assigning a role to a user in a project.
635
tiernoad6d5332020-02-19 14:29:49 +0000636 :param user_obj: user object, obtained with keystone.users.get or list.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100637 :param project: project name.
638 :param role: role name.
639 :raises AuthconnOperationException: if role assignment failed.
640 """
641 try:
tiernoad6d5332020-02-19 14:29:49 +0000642 try:
Eduardo Sousa44603902019-06-04 08:10:32 +0100643 project_obj = self.keystone.projects.get(project)
tiernoad6d5332020-02-19 14:29:49 +0000644 except Exception:
tiernocf042d32019-06-13 09:06:40 +0000645 project_obj_list = self.keystone.projects.list(name=project)
646 if not project_obj_list:
647 raise AuthconnNotFoundException("Project '{}' not found".format(project))
648 project_obj = project_obj_list[0]
Eduardo Sousa44603902019-06-04 08:10:32 +0100649
tiernoad6d5332020-02-19 14:29:49 +0000650 try:
Eduardo Sousa44603902019-06-04 08:10:32 +0100651 role_obj = self.keystone.roles.get(role)
tiernoad6d5332020-02-19 14:29:49 +0000652 except Exception:
tiernocf042d32019-06-13 09:06:40 +0000653 role_obj_list = self.keystone.roles.list(name=role)
654 if not role_obj_list:
655 raise AuthconnNotFoundException("Role '{}' not found".format(role))
656 role_obj = role_obj_list[0]
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100657
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000658 self.keystone.roles.grant(role_obj, user=user_obj, project=project_obj)
tierno4015b472019-06-10 13:57:29 +0000659 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000660 # self.logger.exception("Error during user role assignment using keystone: {}".format(e))
tiernocf042d32019-06-13 09:06:40 +0000661 raise AuthconnOperationException("Error during role '{}' assignment to user '{}' and project '{}' using "
tiernoad6d5332020-02-19 14:29:49 +0000662 "Keystone: {}".format(role, user_obj.name, project, e))
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100663
tiernoad6d5332020-02-19 14:29:49 +0000664 def remove_role_from_user(self, user_obj, project, role):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100665 """
666 Remove a role from a user in a project.
667
tiernoad6d5332020-02-19 14:29:49 +0000668 :param user_obj: user object, obtained with keystone.users.get or list.
tiernocf042d32019-06-13 09:06:40 +0000669 :param project: project name or id.
670 :param role: role name or id.
671
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100672 :raises AuthconnOperationException: if role assignment revocation failed.
673 """
674 try:
tiernoad6d5332020-02-19 14:29:49 +0000675 try:
tiernocf042d32019-06-13 09:06:40 +0000676 project_obj = self.keystone.projects.get(project)
tiernoad6d5332020-02-19 14:29:49 +0000677 except Exception:
tiernocf042d32019-06-13 09:06:40 +0000678 project_obj_list = self.keystone.projects.list(name=project)
679 if not project_obj_list:
680 raise AuthconnNotFoundException("Project '{}' not found".format(project))
681 project_obj = project_obj_list[0]
682
tiernoad6d5332020-02-19 14:29:49 +0000683 try:
tiernocf042d32019-06-13 09:06:40 +0000684 role_obj = self.keystone.roles.get(role)
tiernoad6d5332020-02-19 14:29:49 +0000685 except Exception:
tiernocf042d32019-06-13 09:06:40 +0000686 role_obj_list = self.keystone.roles.list(name=role)
687 if not role_obj_list:
688 raise AuthconnNotFoundException("Role '{}' not found".format(role))
689 role_obj = role_obj_list[0]
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100690
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000691 self.keystone.roles.revoke(role_obj, user=user_obj, project=project_obj)
tierno4015b472019-06-10 13:57:29 +0000692 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000693 # self.logger.exception("Error during user role revocation using keystone: {}".format(e))
tiernocf042d32019-06-13 09:06:40 +0000694 raise AuthconnOperationException("Error during role '{}' revocation to user '{}' and project '{}' using "
tiernoad6d5332020-02-19 14:29:49 +0000695 "Keystone: {}".format(role, user_obj.name, project, e))