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