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