blob: 408b72f2a08dba8ef5eda256297f4ecd8063cc94 [file] [log] [blame]
Eduardo Sousa819d34c2018-07-31 01:20:02 +01001# -*- coding: utf-8 -*-
2
Eduardo Sousad795f872019-02-05 16:05:53 +00003# Copyright 2018 Whitestack, LLC
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
16#
17# For those usages not covered by the Apache License, Version 2.0 please
18# contact: esousa@whitestack.com or glavado@whitestack.com
19##
20
Eduardo Sousa819d34c2018-07-31 01:20:02 +010021"""
22AuthconnKeystone implements implements the connector for
23Openstack Keystone and leverages the RBAC model, to bring
24it for OSM.
25"""
Eduardo Sousa44603902019-06-04 08:10:32 +010026
Eduardo Sousa819d34c2018-07-31 01:20:02 +010027
tierno23acf402019-08-28 13:36:34 +000028__author__ = "Eduardo Sousa <esousa@whitestack.com>, " \
29 "Pedro de la Cruz Ramos <pdelacruzramos@altran.com>"
Eduardo Sousa819d34c2018-07-31 01:20:02 +010030__date__ = "$27-jul-2018 23:59:59$"
31
tierno23acf402019-08-28 13:36:34 +000032from osm_nbi.authconn import Authconn, AuthException, AuthconnOperationException, AuthconnNotFoundException, \
tierno1f029d82019-06-13 22:37:04 +000033 AuthconnConflictException
Eduardo Sousa819d34c2018-07-31 01:20:02 +010034
35import logging
Eduardo Sousa29933fc2018-11-14 06:36:35 +000036import requests
Eduardo Sousa44603902019-06-04 08:10:32 +010037import time
Eduardo Sousa819d34c2018-07-31 01:20:02 +010038from keystoneauth1 import session
39from keystoneauth1.identity import v3
40from keystoneauth1.exceptions.base import ClientException
Eduardo Sousa29933fc2018-11-14 06:36:35 +000041from keystoneauth1.exceptions.http import Conflict
Eduardo Sousa819d34c2018-07-31 01:20:02 +010042from keystoneclient.v3 import client
43from http import HTTPStatus
K Sai Kiran990ac462020-05-20 12:25:12 +053044from osm_nbi.validation import is_valid_uuid, validate_input, http_schema
Eduardo Sousa819d34c2018-07-31 01:20:02 +010045
46
47class AuthconnKeystone(Authconn):
tierno9e87a7f2020-03-23 09:24:10 +000048 def __init__(self, config, db, role_permissions):
49 Authconn.__init__(self, config, db, role_permissions)
Eduardo Sousa819d34c2018-07-31 01:20:02 +010050
51 self.logger = logging.getLogger("nbi.authenticator.keystone")
tiernoad6d5332020-02-19 14:29:49 +000052 self.domains_id2name = {}
53 self.domains_name2id = {}
Eduardo Sousa819d34c2018-07-31 01:20:02 +010054
K Sai Kiran990ac462020-05-20 12:25:12 +053055 self.auth_url = config.get("auth_url")
56 if config.get("auth_url"):
57 validate_input(self.auth_url, http_schema)
58 else:
59 self.auth_url = "http://{0}:{1}/v3".format(config.get("auth_host", "keystone"),
60 config.get("auth_port", "5000"))
tierno6486f742020-02-13 16:30:14 +000061 self.user_domain_name_list = config.get("user_domain_name", "default")
62 self.user_domain_name_list = self.user_domain_name_list.split(",")
tiernoad6d5332020-02-19 14:29:49 +000063 # read only domain list
64 self.user_domain_ro_list = [x[:-3] for x in self.user_domain_name_list if x.endswith(":ro")]
65 # remove the ":ro"
66 self.user_domain_name_list = [x if not x.endswith(":ro") else x[:-3] for x in self.user_domain_name_list]
67
Eduardo Sousa819d34c2018-07-31 01:20:02 +010068 self.admin_project = config.get("service_project", "service")
69 self.admin_username = config.get("service_username", "nbi")
70 self.admin_password = config.get("service_password", "nbi")
tierno6486f742020-02-13 16:30:14 +000071 self.project_domain_name_list = config.get("project_domain_name", "default")
72 self.project_domain_name_list = self.project_domain_name_list.split(",")
73 if len(self.user_domain_name_list) != len(self.project_domain_name_list):
74 raise ValueError("Invalid configuration parameter fo authenticate. 'project_domain_name' and "
75 "'user_domain_name' must be a comma-separated list with the same size. Revise "
76 "configuration or/and 'OSMNBI_AUTHENTICATION_PROJECT_DOMAIN_NAME', "
77 "'OSMNBI_AUTHENTICATION_USER_DOMAIN_NAME' Variables")
Eduardo Sousa819d34c2018-07-31 01:20:02 +010078
Eduardo Sousa29933fc2018-11-14 06:36:35 +000079 # Waiting for Keystone to be up
80 available = None
81 counter = 300
82 while available is None:
83 time.sleep(1)
84 try:
85 result = requests.get(self.auth_url)
86 available = True if result.status_code == 200 else None
87 except Exception:
88 counter -= 1
89 if counter == 0:
90 raise AuthException("Keystone not available after 300s timeout")
91
tierno6486f742020-02-13 16:30:14 +000092 self.auth = v3.Password(user_domain_name=self.user_domain_name_list[0],
Eduardo Sousa819d34c2018-07-31 01:20:02 +010093 username=self.admin_username,
94 password=self.admin_password,
tierno6486f742020-02-13 16:30:14 +000095 project_domain_name=self.project_domain_name_list[0],
Eduardo Sousa819d34c2018-07-31 01:20:02 +010096 project_name=self.admin_project,
97 auth_url=self.auth_url)
98 self.sess = session.Session(auth=self.auth)
K Sai Kiran990ac462020-05-20 12:25:12 +053099 self.keystone = client.Client(session=self.sess, endpoint_override=self.auth_url)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100100
tierno6486f742020-02-13 16:30:14 +0000101 def authenticate(self, credentials, token_info=None):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100102 """
tierno701018c2019-06-25 11:13:14 +0000103 Authenticate a user using username/password or token_info, plus project
tierno6486f742020-02-13 16:30:14 +0000104 :param credentials: dictionary that contains:
105 username: name, id or None
106 password: password or None
107 project_id: name, id, or None. If None first found project will be used to get an scope token
108 project_domain_name: (Optional) To use a concrete domain for the project
109 user_domain_name: (Optional) To use a concrete domain for the project
110 other items are allowed and ignored
tierno701018c2019-06-25 11:13:14 +0000111 :param token_info: previous token_info to obtain authorization
tierno38dcfeb2019-06-10 16:44:00 +0000112 :return: the scoped token info or raises an exception. The token is a dictionary with:
113 _id: token string id,
114 username: username,
115 project_id: scoped_token project_id,
116 project_name: scoped_token project_name,
117 expires: epoch time when it expires,
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100118
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100119 """
tierno6486f742020-02-13 16:30:14 +0000120 username = None
121 user_id = None
122 project_id = None
123 project_name = None
124 if credentials.get("project_domain_name"):
125 project_domain_name_list = (credentials["project_domain_name"], )
126 else:
127 project_domain_name_list = self.project_domain_name_list
128 if credentials.get("user_domain_name"):
129 user_domain_name_list = (credentials["user_domain_name"], )
130 else:
131 user_domain_name_list = self.user_domain_name_list
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100132
tierno6486f742020-02-13 16:30:14 +0000133 for index, project_domain_name in enumerate(project_domain_name_list):
134 user_domain_name = user_domain_name_list[index]
135 try:
136 if credentials.get("username"):
137 if is_valid_uuid(credentials["username"]):
138 user_id = credentials["username"]
139 else:
140 username = credentials["username"]
141
142 # get an unscoped token firstly
143 unscoped_token = self.keystone.get_raw_token_from_identity_service(
144 auth_url=self.auth_url,
145 user_id=user_id,
146 username=username,
147 password=credentials.get("password"),
148 user_domain_name=user_domain_name,
149 project_domain_name=project_domain_name)
150 elif token_info:
151 unscoped_token = self.keystone.tokens.validate(token=token_info.get("_id"))
tierno38dcfeb2019-06-10 16:44:00 +0000152 else:
tierno6486f742020-02-13 16:30:14 +0000153 raise AuthException("Provide credentials: username/password or Authorization Bearer token",
154 http_code=HTTPStatus.UNAUTHORIZED)
tierno38dcfeb2019-06-10 16:44:00 +0000155
tierno6486f742020-02-13 16:30:14 +0000156 if not credentials.get("project_id"):
157 # get first project for the user
158 project_list = self.keystone.projects.list(user=unscoped_token["user"]["id"])
159 if not project_list:
160 raise AuthException("The user {} has not any project and cannot be used for authentication".
161 format(credentials.get("username")), http_code=HTTPStatus.UNAUTHORIZED)
162 project_id = project_list[0].id
163 else:
164 if is_valid_uuid(credentials["project_id"]):
165 project_id = credentials["project_id"]
166 else:
167 project_name = credentials["project_id"]
168
169 scoped_token = self.keystone.get_raw_token_from_identity_service(
tierno38dcfeb2019-06-10 16:44:00 +0000170 auth_url=self.auth_url,
tierno6486f742020-02-13 16:30:14 +0000171 project_name=project_name,
172 project_id=project_id,
173 user_domain_name=user_domain_name,
174 project_domain_name=project_domain_name,
175 token=unscoped_token["auth_token"])
tierno38dcfeb2019-06-10 16:44:00 +0000176
tierno6486f742020-02-13 16:30:14 +0000177 auth_token = {
178 "_id": scoped_token.auth_token,
179 "id": scoped_token.auth_token,
180 "user_id": scoped_token.user_id,
181 "username": scoped_token.username,
182 "project_id": scoped_token.project_id,
183 "project_name": scoped_token.project_name,
184 "project_domain_name": scoped_token.project_domain_name,
185 "user_domain_name": scoped_token.user_domain_name,
186 "expires": scoped_token.expires.timestamp(),
187 "issued_at": scoped_token.issued.timestamp()
188 }
tierno38dcfeb2019-06-10 16:44:00 +0000189
tierno6486f742020-02-13 16:30:14 +0000190 return auth_token
191 except ClientException as e:
192 if index >= len(user_domain_name_list)-1 or index >= len(project_domain_name_list)-1:
193 # if last try, launch exception
194 # self.logger.exception("Error during user authentication using keystone: {}".format(e))
195 raise AuthException("Error during user authentication using Keystone: {}".format(e),
196 http_code=HTTPStatus.UNAUTHORIZED)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100197
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100198 def validate_token(self, token):
199 """
200 Check if the token is valid.
201
tierno701018c2019-06-25 11:13:14 +0000202 :param token: token id to be validated
tiernoa6bb45d2019-06-14 09:45:39 +0000203 :return: dictionary with information associated with the token:
204 "expires":
205 "_id": token_id,
206 "project_id": project_id,
207 "username": ,
208 "roles": list with dict containing {name, id}
209 If the token is not valid an exception is raised.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100210 """
211 if not token:
212 return
213
214 try:
215 token_info = self.keystone.tokens.validate(token=token)
tiernoa6bb45d2019-06-14 09:45:39 +0000216 ses = {
217 "_id": token_info["auth_token"],
tierno701018c2019-06-25 11:13:14 +0000218 "id": token_info["auth_token"],
tiernoa6bb45d2019-06-14 09:45:39 +0000219 "project_id": token_info["project"]["id"],
220 "project_name": token_info["project"]["name"],
221 "user_id": token_info["user"]["id"],
222 "username": token_info["user"]["name"],
223 "roles": token_info["roles"],
tierno701018c2019-06-25 11:13:14 +0000224 "expires": token_info.expires.timestamp(),
225 "issued_at": token_info.issued.timestamp()
tiernoa6bb45d2019-06-14 09:45:39 +0000226 }
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100227
tiernoa6bb45d2019-06-14 09:45:39 +0000228 return ses
tierno4015b472019-06-10 13:57:29 +0000229 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000230 # self.logger.exception("Error during token validation using keystone: {}".format(e))
tierno4015b472019-06-10 13:57:29 +0000231 raise AuthException("Error during token validation using Keystone: {}".format(e),
232 http_code=HTTPStatus.UNAUTHORIZED)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100233
234 def revoke_token(self, token):
235 """
236 Invalidate a token.
237
238 :param token: token to be revoked
239 """
240 try:
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000241 self.logger.info("Revoking token: " + token)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100242 self.keystone.tokens.revoke_token(token=token)
243
244 return True
tierno4015b472019-06-10 13:57:29 +0000245 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000246 # self.logger.exception("Error during token revocation using keystone: {}".format(e))
tierno4015b472019-06-10 13:57:29 +0000247 raise AuthException("Error during token revocation using Keystone: {}".format(e),
248 http_code=HTTPStatus.UNAUTHORIZED)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100249
tiernoad6d5332020-02-19 14:29:49 +0000250 def _get_domain_id(self, domain_name, fail_if_not_found=True):
251 """
252 Get the domain id from the domain_name
253 :param domain_name: Can be the name or id
254 :param fail_if_not_found: If False it returns None instead of raising an exception if not found
255 :return: str or None/exception if domain is not found
256 """
257 domain_id = self.domains_name2id.get(domain_name)
258 if not domain_id:
259 self._get_domains()
260 domain_id = self.domains_name2id.get(domain_name)
261 if not domain_id and domain_name in self.domains_id2name:
262 # domain_name is already an id
263 return domain_name
264 if not domain_id and fail_if_not_found:
265 raise AuthconnNotFoundException("Domain {} cannot be found".format(domain_name))
266 return domain_id
267
268 def _get_domains(self):
269 """
270 Obtain a dictionary with domain_id to domain_name, stored at self.domains_id2name
271 and from domain_name to domain_id, sored at self.domains_name2id
272 :return: None. Exceptions are ignored
273 """
274 try:
275 domains = self.keystone.domains.list()
276 self.domains_id2name = {x.id: x.name for x in domains}
277 self.domains_name2id = {x.name: x.id for x in domains}
278 except Exception:
279 pass
280
delacruzramo01b15d32019-07-02 14:37:47 +0200281 def create_user(self, user_info):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100282 """
283 Create a user.
284
delacruzramo01b15d32019-07-02 14:37:47 +0200285 :param user_info: full user info.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100286 :raises AuthconnOperationException: if user creation failed.
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100287 :return: returns the id of the user in keystone.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100288 """
289 try:
tiernoad6d5332020-02-19 14:29:49 +0000290
291 if user_info.get("domain_name") and user_info["domain_name"] in self.user_domain_ro_list:
292 raise AuthconnConflictException("Cannot create a user in the read only domain {}".
293 format(user_info["domain_name"]))
294
tierno6486f742020-02-13 16:30:14 +0000295 new_user = self.keystone.users.create(
296 user_info["username"], password=user_info["password"],
tiernoad6d5332020-02-19 14:29:49 +0000297 domain=self._get_domain_id(user_info.get("domain_name", self.user_domain_name_list[0])),
tierno6486f742020-02-13 16:30:14 +0000298 _admin=user_info["_admin"])
delacruzramo01b15d32019-07-02 14:37:47 +0200299 if "project_role_mappings" in user_info.keys():
300 for mapping in user_info["project_role_mappings"]:
tiernoad6d5332020-02-19 14:29:49 +0000301 self.assign_role_to_user(new_user, mapping["project"], mapping["role"])
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100302 return {"username": new_user.name, "_id": new_user.id}
tiernocf042d32019-06-13 09:06:40 +0000303 except Conflict as e:
304 # self.logger.exception("Error during user creation using keystone: {}".format(e))
305 raise AuthconnOperationException(e, http_code=HTTPStatus.CONFLICT)
tierno4015b472019-06-10 13:57:29 +0000306 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000307 # self.logger.exception("Error during user creation using keystone: {}".format(e))
tierno4015b472019-06-10 13:57:29 +0000308 raise AuthconnOperationException("Error during user creation using Keystone: {}".format(e))
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100309
delacruzramo01b15d32019-07-02 14:37:47 +0200310 def update_user(self, user_info):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100311 """
tiernocf042d32019-06-13 09:06:40 +0000312 Change the user name and/or password.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100313
delacruzramo01b15d32019-07-02 14:37:47 +0200314 :param user_info: user info modifications
tiernocf042d32019-06-13 09:06:40 +0000315 :raises AuthconnOperationException: if change failed.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100316 """
317 try:
delacruzramo01b15d32019-07-02 14:37:47 +0200318 user = user_info.get("_id") or user_info.get("username")
tiernoad6d5332020-02-19 14:29:49 +0000319 try:
320 user_obj = self.keystone.users.get(user)
321 except Exception:
322 user_obj = None
323 if not user_obj:
324 for user_domain in self.user_domain_name_list:
325 domain_id = self._get_domain_id(user_domain, fail_if_not_found=False)
326 if not domain_id:
327 continue
328 user_obj_list = self.keystone.users.list(name=user, domain=domain_id)
329 if user_obj_list:
330 user_obj = user_obj_list[0]
331 break
332 else: # user not found
333 raise AuthconnNotFoundException("User '{}' not found".format(user))
334
delacruzramo01b15d32019-07-02 14:37:47 +0200335 user_id = user_obj.id
tiernoad6d5332020-02-19 14:29:49 +0000336 domain_id = user_obj.domain_id
337 domain_name = self.domains_id2name.get(domain_id)
338
339 if domain_name in self.user_domain_ro_list:
340 if user_info.get("password") or user_info.get("username"):
341 raise AuthconnConflictException("Cannot update the user {} belonging to a read only domain {}".
342 format(user, domain_name))
343
344 elif user_info.get("password") or user_info.get("username") \
delacruzramo01b15d32019-07-02 14:37:47 +0200345 or user_info.get("add_project_role_mappings") or user_info.get("remove_project_role_mappings"):
tiernoad6d5332020-02-19 14:29:49 +0000346 # if user_index>0, it is an external domain, that should not be updated
delacruzramo15ec7062019-12-26 10:09:04 +0000347 ctime = user_obj._admin.get("created", 0) if hasattr(user_obj, "_admin") else 0
delacruzramo01b15d32019-07-02 14:37:47 +0200348 self.keystone.users.update(user_id, password=user_info.get("password"), name=user_info.get("username"),
delacruzramo15ec7062019-12-26 10:09:04 +0000349 _admin={"created": ctime, "modified": time.time()})
tiernoad6d5332020-02-19 14:29:49 +0000350
delacruzramo01b15d32019-07-02 14:37:47 +0200351 for mapping in user_info.get("remove_project_role_mappings", []):
tiernoad6d5332020-02-19 14:29:49 +0000352 self.remove_role_from_user(user_obj, mapping["project"], mapping["role"])
delacruzramo01b15d32019-07-02 14:37:47 +0200353 for mapping in user_info.get("add_project_role_mappings", []):
tiernoad6d5332020-02-19 14:29:49 +0000354 self.assign_role_to_user(user_obj, mapping["project"], mapping["role"])
tierno4015b472019-06-10 13:57:29 +0000355 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000356 # self.logger.exception("Error during user password/name update using keystone: {}".format(e))
delacruzramo01b15d32019-07-02 14:37:47 +0200357 raise AuthconnOperationException("Error during user update using Keystone: {}".format(e))
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100358
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100359 def delete_user(self, user_id):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100360 """
361 Delete user.
362
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100363 :param user_id: user identifier.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100364 :raises AuthconnOperationException: if user deletion failed.
365 """
366 try:
tiernoad6d5332020-02-19 14:29:49 +0000367 user_obj = self.keystone.users.get(user_id)
368 domain_id = user_obj.domain_id
369 domain_name = self.domains_id2name.get(domain_id)
370 if domain_name in self.user_domain_ro_list:
371 raise AuthconnConflictException("Cannot delete user {} belonging to a read only domain {}".
372 format(user_id, domain_name))
373
tierno38dcfeb2019-06-10 16:44:00 +0000374 result, detail = self.keystone.users.delete(user_id)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100375 if result.status_code != 204:
tierno38dcfeb2019-06-10 16:44:00 +0000376 raise ClientException("error {} {}".format(result.status_code, detail))
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100377 return True
tierno4015b472019-06-10 13:57:29 +0000378 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000379 # self.logger.exception("Error during user deletion using keystone: {}".format(e))
tierno4015b472019-06-10 13:57:29 +0000380 raise AuthconnOperationException("Error during user deletion using Keystone: {}".format(e))
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100381
tiernocf042d32019-06-13 09:06:40 +0000382 def get_user_list(self, filter_q=None):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100383 """
384 Get user list.
385
tiernoad6d5332020-02-19 14:29:49 +0000386 :param filter_q: dictionary to filter user list by one or several
387 _id:
388 name (username is also admitted). If a user id is equal to the filter name, it is also provided
389 domain_id, domain_name
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100390 :return: returns a list of users.
391 """
392 try:
tiernoad6d5332020-02-19 14:29:49 +0000393 self._get_domains()
394 filter_name = filter_domain = None
tiernocf042d32019-06-13 09:06:40 +0000395 if filter_q:
396 filter_name = filter_q.get("name") or filter_q.get("username")
tiernoad6d5332020-02-19 14:29:49 +0000397 if filter_q.get("domain_name"):
398 filter_domain = self._get_domain_id(filter_q["domain_name"], fail_if_not_found=False)
399 # If domain is not found, use the same name to obtain an empty list
400 filter_domain = filter_domain or filter_q["domain_name"]
401 if filter_q.get("domain_id"):
402 filter_domain = filter_q["domain_id"]
403
404 users = self.keystone.users.list(name=filter_name, domain=filter_domain)
405 # get users from user_domain_name_list[1:], because it will not be provided in case of LDAP
406 if filter_domain is None and len(self.user_domain_name_list) > 1:
407 for user_domain in self.user_domain_name_list[1:]:
408 domain_id = self._get_domain_id(user_domain, fail_if_not_found=False)
409 if not domain_id:
410 continue
411 # find if users of this domain are already provided. In this case ignore
412 for u in users:
413 if u.domain_id == domain_id:
414 break
415 else:
416 users += self.keystone.users.list(name=filter_name, domain=domain_id)
417
418 # if filter name matches a user id, provide it also
419 if filter_name:
420 try:
421 user_obj = self.keystone.users.get(filter_name)
422 if user_obj not in users:
423 users.append(user_obj)
424 except Exception:
425 pass
426
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100427 users = [{
428 "username": user.name,
Eduardo Sousa203bad82019-05-23 01:41:18 +0100429 "_id": user.id,
delacruzramo01b15d32019-07-02 14:37:47 +0200430 "id": user.id,
tiernoad6d5332020-02-19 14:29:49 +0000431 "_admin": user.to_dict().get("_admin", {}), # TODO: REVISE
432 "domain_name": self.domains_id2name.get(user.domain_id)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100433 } for user in users if user.name != self.admin_username]
434
tiernocf042d32019-06-13 09:06:40 +0000435 if filter_q and filter_q.get("_id"):
436 users = [user for user in users if filter_q["_id"] == user["_id"]]
Eduardo Sousa2d5a5152019-05-20 15:41:54 +0100437
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100438 for user in users:
tierno1546f2a2019-08-20 15:38:11 +0000439 user["project_role_mappings"] = []
440 user["projects"] = []
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100441 projects = self.keystone.projects.list(user=user["_id"])
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100442 for project in projects:
tierno1546f2a2019-08-20 15:38:11 +0000443 user["projects"].append(project.name)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100444
tierno1546f2a2019-08-20 15:38:11 +0000445 roles = self.keystone.roles.list(user=user["_id"], project=project.id)
446 for role in roles:
447 prm = {
448 "project": project.id,
449 "project_name": project.name,
450 "role_name": role.name,
451 "role": role.id,
452 }
453 user["project_role_mappings"].append(prm)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100454
455 return users
tierno4015b472019-06-10 13:57:29 +0000456 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000457 # self.logger.exception("Error during user listing using keystone: {}".format(e))
tierno4015b472019-06-10 13:57:29 +0000458 raise AuthconnOperationException("Error during user listing using Keystone: {}".format(e))
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100459
tierno1f029d82019-06-13 22:37:04 +0000460 def get_role_list(self, filter_q=None):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100461 """
462 Get role list.
463
tierno1f029d82019-06-13 22:37:04 +0000464 :param filter_q: dictionary to filter role list by _id and/or name.
Eduardo Sousa37de0912019-05-23 02:17:22 +0100465 :return: returns the list of roles.
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100466 """
467 try:
tierno1f029d82019-06-13 22:37:04 +0000468 filter_name = None
469 if filter_q:
470 filter_name = filter_q.get("name")
471 roles_list = self.keystone.roles.list(name=filter_name)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100472
473 roles = [{
474 "name": role.name,
delacruzramo01b15d32019-07-02 14:37:47 +0200475 "_id": role.id,
476 "_admin": role.to_dict().get("_admin", {}),
477 "permissions": role.to_dict().get("permissions", {})
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100478 } for role in roles_list if role.name != "service"]
479
tierno1f029d82019-06-13 22:37:04 +0000480 if filter_q and filter_q.get("_id"):
481 roles = [role for role in roles if filter_q["_id"] == role["_id"]]
482
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100483 return roles
tierno4015b472019-06-10 13:57:29 +0000484 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000485 # self.logger.exception("Error during user role listing using keystone: {}".format(e))
tierno4015b472019-06-10 13:57:29 +0000486 raise AuthException("Error during user role listing using Keystone: {}".format(e),
487 http_code=HTTPStatus.UNAUTHORIZED)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100488
delacruzramo01b15d32019-07-02 14:37:47 +0200489 def create_role(self, role_info):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100490 """
491 Create a role.
492
delacruzramo01b15d32019-07-02 14:37:47 +0200493 :param role_info: full role info.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100494 :raises AuthconnOperationException: if role creation failed.
495 """
496 try:
delacruzramo01b15d32019-07-02 14:37:47 +0200497 result = self.keystone.roles.create(role_info["name"], permissions=role_info.get("permissions"),
498 _admin=role_info.get("_admin"))
tierno1f029d82019-06-13 22:37:04 +0000499 return result.id
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000500 except Conflict as ex:
tierno1f029d82019-06-13 22:37:04 +0000501 raise AuthconnConflictException(str(ex))
tierno4015b472019-06-10 13:57:29 +0000502 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000503 # self.logger.exception("Error during role creation using keystone: {}".format(e))
tierno4015b472019-06-10 13:57:29 +0000504 raise AuthconnOperationException("Error during role creation using Keystone: {}".format(e))
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100505
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100506 def delete_role(self, role_id):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100507 """
508 Delete a role.
509
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100510 :param role_id: role identifier.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100511 :raises AuthconnOperationException: if role deletion failed.
512 """
513 try:
tierno1f029d82019-06-13 22:37:04 +0000514 result, detail = self.keystone.roles.delete(role_id)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100515
516 if result.status_code != 204:
tierno38dcfeb2019-06-10 16:44:00 +0000517 raise ClientException("error {} {}".format(result.status_code, detail))
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100518
519 return True
tierno4015b472019-06-10 13:57:29 +0000520 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000521 # self.logger.exception("Error during role deletion using keystone: {}".format(e))
tierno4015b472019-06-10 13:57:29 +0000522 raise AuthconnOperationException("Error during role deletion using Keystone: {}".format(e))
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100523
delacruzramo01b15d32019-07-02 14:37:47 +0200524 def update_role(self, role_info):
tierno1f029d82019-06-13 22:37:04 +0000525 """
526 Change the name of a role
delacruzramo01b15d32019-07-02 14:37:47 +0200527 :param role_info: full role info
tierno1f029d82019-06-13 22:37:04 +0000528 :return: None
529 """
530 try:
delacruzramo01b15d32019-07-02 14:37:47 +0200531 rid = role_info["_id"]
532 if not is_valid_uuid(rid): # Is this required?
533 role_obj_list = self.keystone.roles.list(name=rid)
tierno1f029d82019-06-13 22:37:04 +0000534 if not role_obj_list:
delacruzramo01b15d32019-07-02 14:37:47 +0200535 raise AuthconnNotFoundException("Role '{}' not found".format(rid))
536 rid = role_obj_list[0].id
537 self.keystone.roles.update(rid, name=role_info["name"], permissions=role_info.get("permissions"),
538 _admin=role_info.get("_admin"))
tierno1f029d82019-06-13 22:37:04 +0000539 except ClientException as e:
540 # self.logger.exception("Error during role update using keystone: {}".format(e))
541 raise AuthconnOperationException("Error during role updating using Keystone: {}".format(e))
542
tierno38dcfeb2019-06-10 16:44:00 +0000543 def get_project_list(self, filter_q=None):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100544 """
545 Get all the projects.
546
Eduardo Sousafa54cd92019-05-20 15:58:41 +0100547 :param filter_q: dictionary to filter project list.
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100548 :return: list of projects
549 """
550 try:
tiernoad6d5332020-02-19 14:29:49 +0000551 self._get_domains()
552 filter_name = filter_domain = None
tierno38dcfeb2019-06-10 16:44:00 +0000553 if filter_q:
554 filter_name = filter_q.get("name")
tiernoad6d5332020-02-19 14:29:49 +0000555 if filter_q.get("domain_name"):
556 filter_domain = self.domains_name2id.get(filter_q["domain_name"])
557 if filter_q.get("domain_id"):
558 filter_domain = filter_q["domain_id"]
559
560 projects = self.keystone.projects.list(name=filter_name, domain=filter_domain)
tierno38dcfeb2019-06-10 16:44:00 +0000561
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100562 projects = [{
563 "name": project.name,
delacruzramo01b15d32019-07-02 14:37:47 +0200564 "_id": project.id,
delacruzramo32bab472019-09-13 12:24:22 +0200565 "_admin": project.to_dict().get("_admin", {}), # TODO: REVISE
566 "quotas": project.to_dict().get("quotas", {}), # TODO: REVISE
tiernoad6d5332020-02-19 14:29:49 +0000567 "domain_name": self.domains_id2name.get(project.domain_id)
tierno38dcfeb2019-06-10 16:44:00 +0000568 } for project in projects]
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100569
tierno38dcfeb2019-06-10 16:44:00 +0000570 if filter_q and filter_q.get("_id"):
Eduardo Sousafa54cd92019-05-20 15:58:41 +0100571 projects = [project for project in projects
tierno38dcfeb2019-06-10 16:44:00 +0000572 if filter_q["_id"] == project["_id"]]
Eduardo Sousafa54cd92019-05-20 15:58:41 +0100573
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100574 return projects
tierno4015b472019-06-10 13:57:29 +0000575 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000576 # self.logger.exception("Error during user project listing using keystone: {}".format(e))
tierno4015b472019-06-10 13:57:29 +0000577 raise AuthException("Error during user project listing using Keystone: {}".format(e),
578 http_code=HTTPStatus.UNAUTHORIZED)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100579
delacruzramo01b15d32019-07-02 14:37:47 +0200580 def create_project(self, project_info):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100581 """
582 Create a project.
583
delacruzramo01b15d32019-07-02 14:37:47 +0200584 :param project_info: full project info.
tierno4015b472019-06-10 13:57:29 +0000585 :return: the internal id of the created project
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100586 :raises AuthconnOperationException: if project creation failed.
587 """
588 try:
tierno6486f742020-02-13 16:30:14 +0000589 result = self.keystone.projects.create(
590 project_info["name"],
tiernoad6d5332020-02-19 14:29:49 +0000591 domain=self._get_domain_id(project_info.get("domain_name", self.project_domain_name_list[0])),
tierno6486f742020-02-13 16:30:14 +0000592 _admin=project_info["_admin"],
593 quotas=project_info.get("quotas", {})
594 )
tierno4015b472019-06-10 13:57:29 +0000595 return result.id
596 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000597 # self.logger.exception("Error during project creation using keystone: {}".format(e))
tierno4015b472019-06-10 13:57:29 +0000598 raise AuthconnOperationException("Error during project creation using Keystone: {}".format(e))
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100599
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100600 def delete_project(self, project_id):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100601 """
602 Delete a project.
603
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100604 :param project_id: project identifier.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100605 :raises AuthconnOperationException: if project deletion failed.
606 """
607 try:
tierno38dcfeb2019-06-10 16:44:00 +0000608 # projects = self.keystone.projects.list()
609 # project_obj = [project for project in projects if project.id == project_id][0]
610 # result, _ = self.keystone.projects.delete(project_obj)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100611
tierno38dcfeb2019-06-10 16:44:00 +0000612 result, detail = self.keystone.projects.delete(project_id)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100613 if result.status_code != 204:
tierno38dcfeb2019-06-10 16:44:00 +0000614 raise ClientException("error {} {}".format(result.status_code, detail))
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100615
616 return True
tierno4015b472019-06-10 13:57:29 +0000617 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000618 # self.logger.exception("Error during project deletion using keystone: {}".format(e))
tierno4015b472019-06-10 13:57:29 +0000619 raise AuthconnOperationException("Error during project deletion using Keystone: {}".format(e))
620
delacruzramo01b15d32019-07-02 14:37:47 +0200621 def update_project(self, project_id, project_info):
tierno4015b472019-06-10 13:57:29 +0000622 """
623 Change the name of a project
624 :param project_id: project to be changed
delacruzramo01b15d32019-07-02 14:37:47 +0200625 :param project_info: full project info
tierno4015b472019-06-10 13:57:29 +0000626 :return: None
627 """
628 try:
delacruzramo32bab472019-09-13 12:24:22 +0200629 self.keystone.projects.update(project_id, name=project_info["name"],
630 _admin=project_info["_admin"],
631 quotas=project_info.get("quotas", {})
632 )
tierno4015b472019-06-10 13:57:29 +0000633 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000634 # self.logger.exception("Error during project update using keystone: {}".format(e))
delacruzramo01b15d32019-07-02 14:37:47 +0200635 raise AuthconnOperationException("Error during project update using Keystone: {}".format(e))
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100636
tiernoad6d5332020-02-19 14:29:49 +0000637 def assign_role_to_user(self, user_obj, project, role):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100638 """
639 Assigning a role to a user in a project.
640
tiernoad6d5332020-02-19 14:29:49 +0000641 :param user_obj: user object, obtained with keystone.users.get or list.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100642 :param project: project name.
643 :param role: role name.
644 :raises AuthconnOperationException: if role assignment failed.
645 """
646 try:
tiernoad6d5332020-02-19 14:29:49 +0000647 try:
Eduardo Sousa44603902019-06-04 08:10:32 +0100648 project_obj = self.keystone.projects.get(project)
tiernoad6d5332020-02-19 14:29:49 +0000649 except Exception:
tiernocf042d32019-06-13 09:06:40 +0000650 project_obj_list = self.keystone.projects.list(name=project)
651 if not project_obj_list:
652 raise AuthconnNotFoundException("Project '{}' not found".format(project))
653 project_obj = project_obj_list[0]
Eduardo Sousa44603902019-06-04 08:10:32 +0100654
tiernoad6d5332020-02-19 14:29:49 +0000655 try:
Eduardo Sousa44603902019-06-04 08:10:32 +0100656 role_obj = self.keystone.roles.get(role)
tiernoad6d5332020-02-19 14:29:49 +0000657 except Exception:
tiernocf042d32019-06-13 09:06:40 +0000658 role_obj_list = self.keystone.roles.list(name=role)
659 if not role_obj_list:
660 raise AuthconnNotFoundException("Role '{}' not found".format(role))
661 role_obj = role_obj_list[0]
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100662
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000663 self.keystone.roles.grant(role_obj, user=user_obj, project=project_obj)
tierno4015b472019-06-10 13:57:29 +0000664 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000665 # self.logger.exception("Error during user role assignment using keystone: {}".format(e))
tiernocf042d32019-06-13 09:06:40 +0000666 raise AuthconnOperationException("Error during role '{}' assignment to user '{}' and project '{}' using "
tiernoad6d5332020-02-19 14:29:49 +0000667 "Keystone: {}".format(role, user_obj.name, project, e))
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100668
tiernoad6d5332020-02-19 14:29:49 +0000669 def remove_role_from_user(self, user_obj, project, role):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100670 """
671 Remove a role from a user in a project.
672
tiernoad6d5332020-02-19 14:29:49 +0000673 :param user_obj: user object, obtained with keystone.users.get or list.
tiernocf042d32019-06-13 09:06:40 +0000674 :param project: project name or id.
675 :param role: role name or id.
676
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100677 :raises AuthconnOperationException: if role assignment revocation failed.
678 """
679 try:
tiernoad6d5332020-02-19 14:29:49 +0000680 try:
tiernocf042d32019-06-13 09:06:40 +0000681 project_obj = self.keystone.projects.get(project)
tiernoad6d5332020-02-19 14:29:49 +0000682 except Exception:
tiernocf042d32019-06-13 09:06:40 +0000683 project_obj_list = self.keystone.projects.list(name=project)
684 if not project_obj_list:
685 raise AuthconnNotFoundException("Project '{}' not found".format(project))
686 project_obj = project_obj_list[0]
687
tiernoad6d5332020-02-19 14:29:49 +0000688 try:
tiernocf042d32019-06-13 09:06:40 +0000689 role_obj = self.keystone.roles.get(role)
tiernoad6d5332020-02-19 14:29:49 +0000690 except Exception:
tiernocf042d32019-06-13 09:06:40 +0000691 role_obj_list = self.keystone.roles.list(name=role)
692 if not role_obj_list:
693 raise AuthconnNotFoundException("Role '{}' not found".format(role))
694 role_obj = role_obj_list[0]
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100695
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000696 self.keystone.roles.revoke(role_obj, user=user_obj, project=project_obj)
tierno4015b472019-06-10 13:57:29 +0000697 except ClientException as e:
tierno701018c2019-06-25 11:13:14 +0000698 # self.logger.exception("Error during user role revocation using keystone: {}".format(e))
tiernocf042d32019-06-13 09:06:40 +0000699 raise AuthconnOperationException("Error during role '{}' revocation to user '{}' and project '{}' using "
tiernoad6d5332020-02-19 14:29:49 +0000700 "Keystone: {}".format(role, user_obj.name, project, e))