748b2e700c16da6b809f0bb96d14787e00909c84
[osm/NBI.git] / osm_nbi / authconn_keystone.py
1 # -*- coding: utf-8 -*-
2
3 # 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
21 """
22 AuthconnKeystone implements implements the connector for
23 Openstack Keystone and leverages the RBAC model, to bring
24 it for OSM.
25 """
26
27
28 __author__ = "Eduardo Sousa <esousa@whitestack.com>, " \
29 "Pedro de la Cruz Ramos <pdelacruzramos@altran.com>"
30 __date__ = "$27-jul-2018 23:59:59$"
31
32 from osm_nbi.authconn import Authconn, AuthException, AuthconnOperationException, AuthconnNotFoundException, \
33 AuthconnConflictException
34
35 import logging
36 import requests
37 import time
38 from keystoneauth1 import session
39 from keystoneauth1.identity import v3
40 from keystoneauth1.exceptions.base import ClientException
41 from keystoneauth1.exceptions.http import Conflict
42 from keystoneclient.v3 import client
43 from http import HTTPStatus
44 from osm_nbi.validation import is_valid_uuid
45
46
47 class AuthconnKeystone(Authconn):
48 def __init__(self, config, db):
49 Authconn.__init__(self, config, db)
50
51 self.logger = logging.getLogger("nbi.authenticator.keystone")
52
53 self.auth_url = "http://{0}:{1}/v3".format(config.get("auth_url", "keystone"), config.get("auth_port", "5000"))
54 self.user_domain_name_list = config.get("user_domain_name", "default")
55 self.user_domain_name_list = self.user_domain_name_list.split(",")
56 self.admin_project = config.get("service_project", "service")
57 self.admin_username = config.get("service_username", "nbi")
58 self.admin_password = config.get("service_password", "nbi")
59 self.project_domain_name_list = config.get("project_domain_name", "default")
60 self.project_domain_name_list = self.project_domain_name_list.split(",")
61 if len(self.user_domain_name_list) != len(self.project_domain_name_list):
62 raise ValueError("Invalid configuration parameter fo authenticate. 'project_domain_name' and "
63 "'user_domain_name' must be a comma-separated list with the same size. Revise "
64 "configuration or/and 'OSMNBI_AUTHENTICATION_PROJECT_DOMAIN_NAME', "
65 "'OSMNBI_AUTHENTICATION_USER_DOMAIN_NAME' Variables")
66
67 # Waiting for Keystone to be up
68 available = None
69 counter = 300
70 while available is None:
71 time.sleep(1)
72 try:
73 result = requests.get(self.auth_url)
74 available = True if result.status_code == 200 else None
75 except Exception:
76 counter -= 1
77 if counter == 0:
78 raise AuthException("Keystone not available after 300s timeout")
79
80 self.auth = v3.Password(user_domain_name=self.user_domain_name_list[0],
81 username=self.admin_username,
82 password=self.admin_password,
83 project_domain_name=self.project_domain_name_list[0],
84 project_name=self.admin_project,
85 auth_url=self.auth_url)
86 self.sess = session.Session(auth=self.auth)
87 self.keystone = client.Client(session=self.sess)
88
89 def authenticate(self, credentials, token_info=None):
90 """
91 Authenticate a user using username/password or token_info, plus project
92 :param credentials: dictionary that contains:
93 username: name, id or None
94 password: password or None
95 project_id: name, id, or None. If None first found project will be used to get an scope token
96 project_domain_name: (Optional) To use a concrete domain for the project
97 user_domain_name: (Optional) To use a concrete domain for the project
98 other items are allowed and ignored
99 :param token_info: previous token_info to obtain authorization
100 :return: the scoped token info or raises an exception. The token is a dictionary with:
101 _id: token string id,
102 username: username,
103 project_id: scoped_token project_id,
104 project_name: scoped_token project_name,
105 expires: epoch time when it expires,
106
107 """
108 username = None
109 user_id = None
110 project_id = None
111 project_name = None
112 if credentials.get("project_domain_name"):
113 project_domain_name_list = (credentials["project_domain_name"], )
114 else:
115 project_domain_name_list = self.project_domain_name_list
116 if credentials.get("user_domain_name"):
117 user_domain_name_list = (credentials["user_domain_name"], )
118 else:
119 user_domain_name_list = self.user_domain_name_list
120
121 for index, project_domain_name in enumerate(project_domain_name_list):
122 user_domain_name = user_domain_name_list[index]
123 try:
124 if credentials.get("username"):
125 if is_valid_uuid(credentials["username"]):
126 user_id = credentials["username"]
127 else:
128 username = credentials["username"]
129
130 # get an unscoped token firstly
131 unscoped_token = self.keystone.get_raw_token_from_identity_service(
132 auth_url=self.auth_url,
133 user_id=user_id,
134 username=username,
135 password=credentials.get("password"),
136 user_domain_name=user_domain_name,
137 project_domain_name=project_domain_name)
138 elif token_info:
139 unscoped_token = self.keystone.tokens.validate(token=token_info.get("_id"))
140 else:
141 raise AuthException("Provide credentials: username/password or Authorization Bearer token",
142 http_code=HTTPStatus.UNAUTHORIZED)
143
144 if not credentials.get("project_id"):
145 # get first project for the user
146 project_list = self.keystone.projects.list(user=unscoped_token["user"]["id"])
147 if not project_list:
148 raise AuthException("The user {} has not any project and cannot be used for authentication".
149 format(credentials.get("username")), http_code=HTTPStatus.UNAUTHORIZED)
150 project_id = project_list[0].id
151 else:
152 if is_valid_uuid(credentials["project_id"]):
153 project_id = credentials["project_id"]
154 else:
155 project_name = credentials["project_id"]
156
157 scoped_token = self.keystone.get_raw_token_from_identity_service(
158 auth_url=self.auth_url,
159 project_name=project_name,
160 project_id=project_id,
161 user_domain_name=user_domain_name,
162 project_domain_name=project_domain_name,
163 token=unscoped_token["auth_token"])
164
165 auth_token = {
166 "_id": scoped_token.auth_token,
167 "id": scoped_token.auth_token,
168 "user_id": scoped_token.user_id,
169 "username": scoped_token.username,
170 "project_id": scoped_token.project_id,
171 "project_name": scoped_token.project_name,
172 "project_domain_name": scoped_token.project_domain_name,
173 "user_domain_name": scoped_token.user_domain_name,
174 "expires": scoped_token.expires.timestamp(),
175 "issued_at": scoped_token.issued.timestamp()
176 }
177
178 return auth_token
179 except ClientException as e:
180 if index >= len(user_domain_name_list)-1 or index >= len(project_domain_name_list)-1:
181 # if last try, launch exception
182 # self.logger.exception("Error during user authentication using keystone: {}".format(e))
183 raise AuthException("Error during user authentication using Keystone: {}".format(e),
184 http_code=HTTPStatus.UNAUTHORIZED)
185
186 def validate_token(self, token):
187 """
188 Check if the token is valid.
189
190 :param token: token id to be validated
191 :return: dictionary with information associated with the token:
192 "expires":
193 "_id": token_id,
194 "project_id": project_id,
195 "username": ,
196 "roles": list with dict containing {name, id}
197 If the token is not valid an exception is raised.
198 """
199 if not token:
200 return
201
202 try:
203 token_info = self.keystone.tokens.validate(token=token)
204 ses = {
205 "_id": token_info["auth_token"],
206 "id": token_info["auth_token"],
207 "project_id": token_info["project"]["id"],
208 "project_name": token_info["project"]["name"],
209 "user_id": token_info["user"]["id"],
210 "username": token_info["user"]["name"],
211 "roles": token_info["roles"],
212 "expires": token_info.expires.timestamp(),
213 "issued_at": token_info.issued.timestamp()
214 }
215
216 return ses
217 except ClientException as e:
218 # self.logger.exception("Error during token validation using keystone: {}".format(e))
219 raise AuthException("Error during token validation using Keystone: {}".format(e),
220 http_code=HTTPStatus.UNAUTHORIZED)
221
222 def revoke_token(self, token):
223 """
224 Invalidate a token.
225
226 :param token: token to be revoked
227 """
228 try:
229 self.logger.info("Revoking token: " + token)
230 self.keystone.tokens.revoke_token(token=token)
231
232 return True
233 except ClientException as e:
234 # self.logger.exception("Error during token revocation using keystone: {}".format(e))
235 raise AuthException("Error during token revocation using Keystone: {}".format(e),
236 http_code=HTTPStatus.UNAUTHORIZED)
237
238 def create_user(self, user_info):
239 """
240 Create a user.
241
242 :param user_info: full user info.
243 :raises AuthconnOperationException: if user creation failed.
244 :return: returns the id of the user in keystone.
245 """
246 try:
247 new_user = self.keystone.users.create(
248 user_info["username"], password=user_info["password"],
249 domain=user_info.get("user_domain_name", self.user_domain_name_list[0]),
250 _admin=user_info["_admin"])
251 if "project_role_mappings" in user_info.keys():
252 for mapping in user_info["project_role_mappings"]:
253 self.assign_role_to_user(new_user.id, mapping["project"], mapping["role"])
254 return {"username": new_user.name, "_id": new_user.id}
255 except Conflict as e:
256 # self.logger.exception("Error during user creation using keystone: {}".format(e))
257 raise AuthconnOperationException(e, http_code=HTTPStatus.CONFLICT)
258 except ClientException as e:
259 # self.logger.exception("Error during user creation using keystone: {}".format(e))
260 raise AuthconnOperationException("Error during user creation using Keystone: {}".format(e))
261
262 def update_user(self, user_info):
263 """
264 Change the user name and/or password.
265
266 :param user_info: user info modifications
267 :raises AuthconnOperationException: if change failed.
268 """
269 try:
270 user = user_info.get("_id") or user_info.get("username")
271 if is_valid_uuid(user):
272 user_obj_list = [self.keystone.users.get(user)]
273 else:
274 user_obj_list = self.keystone.users.list(name=user)
275 if not user_obj_list:
276 raise AuthconnNotFoundException("User '{}' not found".format(user))
277 user_obj = user_obj_list[0]
278 user_id = user_obj.id
279 if user_info.get("password") or user_info.get("username") \
280 or user_info.get("add_project_role_mappings") or user_info.get("remove_project_role_mappings"):
281 ctime = user_obj._admin.get("created", 0) if hasattr(user_obj, "_admin") else 0
282 self.keystone.users.update(user_id, password=user_info.get("password"), name=user_info.get("username"),
283 _admin={"created": ctime, "modified": time.time()})
284 for mapping in user_info.get("remove_project_role_mappings", []):
285 self.remove_role_from_user(user_id, mapping["project"], mapping["role"])
286 for mapping in user_info.get("add_project_role_mappings", []):
287 self.assign_role_to_user(user_id, mapping["project"], mapping["role"])
288 except ClientException as e:
289 # self.logger.exception("Error during user password/name update using keystone: {}".format(e))
290 raise AuthconnOperationException("Error during user update using Keystone: {}".format(e))
291
292 def delete_user(self, user_id):
293 """
294 Delete user.
295
296 :param user_id: user identifier.
297 :raises AuthconnOperationException: if user deletion failed.
298 """
299 try:
300 result, detail = self.keystone.users.delete(user_id)
301 if result.status_code != 204:
302 raise ClientException("error {} {}".format(result.status_code, detail))
303 return True
304 except ClientException as e:
305 # self.logger.exception("Error during user deletion using keystone: {}".format(e))
306 raise AuthconnOperationException("Error during user deletion using Keystone: {}".format(e))
307
308 def get_user_list(self, filter_q=None):
309 """
310 Get user list.
311
312 :param filter_q: dictionary to filter user list by name (username is also admited) and/or _id
313 :return: returns a list of users.
314 """
315 try:
316 filter_name = None
317 if filter_q:
318 filter_name = filter_q.get("name") or filter_q.get("username")
319 users = self.keystone.users.list(name=filter_name)
320 users = [{
321 "username": user.name,
322 "_id": user.id,
323 "id": user.id,
324 "_admin": user.to_dict().get("_admin", {}) # TODO: REVISE
325 } for user in users if user.name != self.admin_username]
326
327 if filter_q and filter_q.get("_id"):
328 users = [user for user in users if filter_q["_id"] == user["_id"]]
329
330 for user in users:
331 user["project_role_mappings"] = []
332 user["projects"] = []
333 projects = self.keystone.projects.list(user=user["_id"])
334 for project in projects:
335 user["projects"].append(project.name)
336
337 roles = self.keystone.roles.list(user=user["_id"], project=project.id)
338 for role in roles:
339 prm = {
340 "project": project.id,
341 "project_name": project.name,
342 "role_name": role.name,
343 "role": role.id,
344 }
345 user["project_role_mappings"].append(prm)
346
347 return users
348 except ClientException as e:
349 # self.logger.exception("Error during user listing using keystone: {}".format(e))
350 raise AuthconnOperationException("Error during user listing using Keystone: {}".format(e))
351
352 def get_role_list(self, filter_q=None):
353 """
354 Get role list.
355
356 :param filter_q: dictionary to filter role list by _id and/or name.
357 :return: returns the list of roles.
358 """
359 try:
360 filter_name = None
361 if filter_q:
362 filter_name = filter_q.get("name")
363 roles_list = self.keystone.roles.list(name=filter_name)
364
365 roles = [{
366 "name": role.name,
367 "_id": role.id,
368 "_admin": role.to_dict().get("_admin", {}),
369 "permissions": role.to_dict().get("permissions", {})
370 } for role in roles_list if role.name != "service"]
371
372 if filter_q and filter_q.get("_id"):
373 roles = [role for role in roles if filter_q["_id"] == role["_id"]]
374
375 return roles
376 except ClientException as e:
377 # self.logger.exception("Error during user role listing using keystone: {}".format(e))
378 raise AuthException("Error during user role listing using Keystone: {}".format(e),
379 http_code=HTTPStatus.UNAUTHORIZED)
380
381 def create_role(self, role_info):
382 """
383 Create a role.
384
385 :param role_info: full role info.
386 :raises AuthconnOperationException: if role creation failed.
387 """
388 try:
389 result = self.keystone.roles.create(role_info["name"], permissions=role_info.get("permissions"),
390 _admin=role_info.get("_admin"))
391 return result.id
392 except Conflict as ex:
393 raise AuthconnConflictException(str(ex))
394 except ClientException as e:
395 # self.logger.exception("Error during role creation using keystone: {}".format(e))
396 raise AuthconnOperationException("Error during role creation using Keystone: {}".format(e))
397
398 def delete_role(self, role_id):
399 """
400 Delete a role.
401
402 :param role_id: role identifier.
403 :raises AuthconnOperationException: if role deletion failed.
404 """
405 try:
406 result, detail = self.keystone.roles.delete(role_id)
407
408 if result.status_code != 204:
409 raise ClientException("error {} {}".format(result.status_code, detail))
410
411 return True
412 except ClientException as e:
413 # self.logger.exception("Error during role deletion using keystone: {}".format(e))
414 raise AuthconnOperationException("Error during role deletion using Keystone: {}".format(e))
415
416 def update_role(self, role_info):
417 """
418 Change the name of a role
419 :param role_info: full role info
420 :return: None
421 """
422 try:
423 rid = role_info["_id"]
424 if not is_valid_uuid(rid): # Is this required?
425 role_obj_list = self.keystone.roles.list(name=rid)
426 if not role_obj_list:
427 raise AuthconnNotFoundException("Role '{}' not found".format(rid))
428 rid = role_obj_list[0].id
429 self.keystone.roles.update(rid, name=role_info["name"], permissions=role_info.get("permissions"),
430 _admin=role_info.get("_admin"))
431 except ClientException as e:
432 # self.logger.exception("Error during role update using keystone: {}".format(e))
433 raise AuthconnOperationException("Error during role updating using Keystone: {}".format(e))
434
435 def get_project_list(self, filter_q=None):
436 """
437 Get all the projects.
438
439 :param filter_q: dictionary to filter project list.
440 :return: list of projects
441 """
442 try:
443 filter_name = None
444 if filter_q:
445 filter_name = filter_q.get("name")
446 projects = self.keystone.projects.list(name=filter_name)
447
448 projects = [{
449 "name": project.name,
450 "_id": project.id,
451 "_admin": project.to_dict().get("_admin", {}), # TODO: REVISE
452 "quotas": project.to_dict().get("quotas", {}), # TODO: REVISE
453 } for project in projects]
454
455 if filter_q and filter_q.get("_id"):
456 projects = [project for project in projects
457 if filter_q["_id"] == project["_id"]]
458
459 return projects
460 except ClientException as e:
461 # self.logger.exception("Error during user project listing using keystone: {}".format(e))
462 raise AuthException("Error during user project listing using Keystone: {}".format(e),
463 http_code=HTTPStatus.UNAUTHORIZED)
464
465 def create_project(self, project_info):
466 """
467 Create a project.
468
469 :param project_info: full project info.
470 :return: the internal id of the created project
471 :raises AuthconnOperationException: if project creation failed.
472 """
473 try:
474 result = self.keystone.projects.create(
475 project_info["name"],
476 project_info.get("project_domain_name", self.project_domain_name_list[0]),
477 _admin=project_info["_admin"],
478 quotas=project_info.get("quotas", {})
479 )
480 return result.id
481 except ClientException as e:
482 # self.logger.exception("Error during project creation using keystone: {}".format(e))
483 raise AuthconnOperationException("Error during project creation using Keystone: {}".format(e))
484
485 def delete_project(self, project_id):
486 """
487 Delete a project.
488
489 :param project_id: project identifier.
490 :raises AuthconnOperationException: if project deletion failed.
491 """
492 try:
493 # projects = self.keystone.projects.list()
494 # project_obj = [project for project in projects if project.id == project_id][0]
495 # result, _ = self.keystone.projects.delete(project_obj)
496
497 result, detail = self.keystone.projects.delete(project_id)
498 if result.status_code != 204:
499 raise ClientException("error {} {}".format(result.status_code, detail))
500
501 return True
502 except ClientException as e:
503 # self.logger.exception("Error during project deletion using keystone: {}".format(e))
504 raise AuthconnOperationException("Error during project deletion using Keystone: {}".format(e))
505
506 def update_project(self, project_id, project_info):
507 """
508 Change the name of a project
509 :param project_id: project to be changed
510 :param project_info: full project info
511 :return: None
512 """
513 try:
514 self.keystone.projects.update(project_id, name=project_info["name"],
515 _admin=project_info["_admin"],
516 quotas=project_info.get("quotas", {})
517 )
518 except ClientException as e:
519 # self.logger.exception("Error during project update using keystone: {}".format(e))
520 raise AuthconnOperationException("Error during project update using Keystone: {}".format(e))
521
522 def assign_role_to_user(self, user, project, role):
523 """
524 Assigning a role to a user in a project.
525
526 :param user: username.
527 :param project: project name.
528 :param role: role name.
529 :raises AuthconnOperationException: if role assignment failed.
530 """
531 try:
532 if is_valid_uuid(user):
533 user_obj = self.keystone.users.get(user)
534 else:
535 user_obj_list = self.keystone.users.list(name=user)
536 if not user_obj_list:
537 raise AuthconnNotFoundException("User '{}' not found".format(user))
538 user_obj = user_obj_list[0]
539
540 if is_valid_uuid(project):
541 project_obj = self.keystone.projects.get(project)
542 else:
543 project_obj_list = self.keystone.projects.list(name=project)
544 if not project_obj_list:
545 raise AuthconnNotFoundException("Project '{}' not found".format(project))
546 project_obj = project_obj_list[0]
547
548 if is_valid_uuid(role):
549 role_obj = self.keystone.roles.get(role)
550 else:
551 role_obj_list = self.keystone.roles.list(name=role)
552 if not role_obj_list:
553 raise AuthconnNotFoundException("Role '{}' not found".format(role))
554 role_obj = role_obj_list[0]
555
556 self.keystone.roles.grant(role_obj, user=user_obj, project=project_obj)
557 except ClientException as e:
558 # self.logger.exception("Error during user role assignment using keystone: {}".format(e))
559 raise AuthconnOperationException("Error during role '{}' assignment to user '{}' and project '{}' using "
560 "Keystone: {}".format(role, user, project, e))
561
562 def remove_role_from_user(self, user, project, role):
563 """
564 Remove a role from a user in a project.
565
566 :param user: username.
567 :param project: project name or id.
568 :param role: role name or id.
569
570 :raises AuthconnOperationException: if role assignment revocation failed.
571 """
572 try:
573 if is_valid_uuid(user):
574 user_obj = self.keystone.users.get(user)
575 else:
576 user_obj_list = self.keystone.users.list(name=user)
577 if not user_obj_list:
578 raise AuthconnNotFoundException("User '{}' not found".format(user))
579 user_obj = user_obj_list[0]
580
581 if is_valid_uuid(project):
582 project_obj = self.keystone.projects.get(project)
583 else:
584 project_obj_list = self.keystone.projects.list(name=project)
585 if not project_obj_list:
586 raise AuthconnNotFoundException("Project '{}' not found".format(project))
587 project_obj = project_obj_list[0]
588
589 if is_valid_uuid(role):
590 role_obj = self.keystone.roles.get(role)
591 else:
592 role_obj_list = self.keystone.roles.list(name=role)
593 if not role_obj_list:
594 raise AuthconnNotFoundException("Role '{}' not found".format(role))
595 role_obj = role_obj_list[0]
596
597 self.keystone.roles.revoke(role_obj, user=user_obj, project=project_obj)
598 except ClientException as e:
599 # self.logger.exception("Error during user role revocation using keystone: {}".format(e))
600 raise AuthconnOperationException("Error during role '{}' revocation to user '{}' and project '{}' using "
601 "Keystone: {}".format(role, user, project, e))