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