115de7c8b8a15e5aee1ed0736494d09d624490ba
[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, db, token_cache):
48 Authconn.__init__(self, config, db, token_cache)
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_info=None):
82 """
83 Authenticate a user using username/password or token_info, 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_info: previous token_info 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_info:
117 unscoped_token = self.keystone.tokens.validate(token=token_info.get("_id"))
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 "id": scoped_token.auth_token,
146 "user_id": scoped_token.user_id,
147 "username": scoped_token.username,
148 "project_id": scoped_token.project_id,
149 "project_name": scoped_token.project_name,
150 "expires": scoped_token.expires.timestamp(),
151 "issued_at": scoped_token.issued.timestamp()
152 }
153
154 return auth_token
155 except ClientException as e:
156 # self.logger.exception("Error during user authentication using keystone. Method: basic: {}".format(e))
157 raise AuthException("Error during user authentication using Keystone: {}".format(e),
158 http_code=HTTPStatus.UNAUTHORIZED)
159
160 def validate_token(self, token):
161 """
162 Check if the token is valid.
163
164 :param token: token id to be validated
165 :return: dictionary with information associated with the token:
166 "expires":
167 "_id": token_id,
168 "project_id": project_id,
169 "username": ,
170 "roles": list with dict containing {name, id}
171 If the token is not valid an exception is raised.
172 """
173 if not token:
174 return
175
176 try:
177 token_info = self.keystone.tokens.validate(token=token)
178 ses = {
179 "_id": token_info["auth_token"],
180 "id": token_info["auth_token"],
181 "project_id": token_info["project"]["id"],
182 "project_name": token_info["project"]["name"],
183 "user_id": token_info["user"]["id"],
184 "username": token_info["user"]["name"],
185 "roles": token_info["roles"],
186 "expires": token_info.expires.timestamp(),
187 "issued_at": token_info.issued.timestamp()
188 }
189
190 return ses
191 except ClientException as e:
192 # self.logger.exception("Error during token validation using keystone: {}".format(e))
193 raise AuthException("Error during token validation using Keystone: {}".format(e),
194 http_code=HTTPStatus.UNAUTHORIZED)
195
196 def revoke_token(self, token):
197 """
198 Invalidate a token.
199
200 :param token: token to be revoked
201 """
202 try:
203 self.logger.info("Revoking token: " + token)
204 self.keystone.tokens.revoke_token(token=token)
205
206 return True
207 except ClientException as e:
208 # self.logger.exception("Error during token revocation using keystone: {}".format(e))
209 raise AuthException("Error during token revocation using Keystone: {}".format(e),
210 http_code=HTTPStatus.UNAUTHORIZED)
211
212 def create_user(self, user_info):
213 """
214 Create a user.
215
216 :param user_info: full user info.
217 :raises AuthconnOperationException: if user creation failed.
218 :return: returns the id of the user in keystone.
219 """
220 try:
221 new_user = self.keystone.users.create(user_info["username"], password=user_info["password"],
222 domain=self.user_domain_name, _admin=user_info["_admin"])
223 if "project_role_mappings" in user_info.keys():
224 for mapping in user_info["project_role_mappings"]:
225 self.assign_role_to_user(new_user.id, mapping["project"], mapping["role"])
226 return {"username": new_user.name, "_id": new_user.id}
227 except Conflict as e:
228 # self.logger.exception("Error during user creation using keystone: {}".format(e))
229 raise AuthconnOperationException(e, http_code=HTTPStatus.CONFLICT)
230 except ClientException as e:
231 # self.logger.exception("Error during user creation using keystone: {}".format(e))
232 raise AuthconnOperationException("Error during user creation using Keystone: {}".format(e))
233
234 def update_user(self, user_info):
235 """
236 Change the user name and/or password.
237
238 :param user_info: user info modifications
239 :raises AuthconnOperationException: if change failed.
240 """
241 try:
242 user = user_info.get("_id") or user_info.get("username")
243 if is_valid_uuid(user):
244 user_obj_list = [self.keystone.users.get(user)]
245 else:
246 user_obj_list = self.keystone.users.list(name=user)
247 if not user_obj_list:
248 raise AuthconnNotFoundException("User '{}' not found".format(user))
249 user_obj = user_obj_list[0]
250 user_id = user_obj.id
251 if user_info.get("password") or user_info.get("username") \
252 or user_info.get("add_project_role_mappings") or user_info.get("remove_project_role_mappings"):
253 self.keystone.users.update(user_id, password=user_info.get("password"), name=user_info.get("username"),
254 _admin={"created": user_obj._admin["created"], "modified": time.time()})
255 for mapping in user_info.get("remove_project_role_mappings", []):
256 self.remove_role_from_user(user_id, mapping["project"], mapping["role"])
257 for mapping in user_info.get("add_project_role_mappings", []):
258 self.assign_role_to_user(user_id, mapping["project"], mapping["role"])
259 except ClientException as e:
260 # self.logger.exception("Error during user password/name update using keystone: {}".format(e))
261 raise AuthconnOperationException("Error during user update using Keystone: {}".format(e))
262
263 def delete_user(self, user_id):
264 """
265 Delete user.
266
267 :param user_id: user identifier.
268 :raises AuthconnOperationException: if user deletion failed.
269 """
270 try:
271 result, detail = self.keystone.users.delete(user_id)
272 if result.status_code != 204:
273 raise ClientException("error {} {}".format(result.status_code, detail))
274 return True
275 except ClientException as e:
276 # self.logger.exception("Error during user deletion using keystone: {}".format(e))
277 raise AuthconnOperationException("Error during user deletion using Keystone: {}".format(e))
278
279 def get_user_list(self, filter_q=None):
280 """
281 Get user list.
282
283 :param filter_q: dictionary to filter user list by name (username is also admited) and/or _id
284 :return: returns a list of users.
285 """
286 try:
287 filter_name = None
288 if filter_q:
289 filter_name = filter_q.get("name") or filter_q.get("username")
290 users = self.keystone.users.list(name=filter_name)
291 users = [{
292 "username": user.name,
293 "_id": user.id,
294 "id": user.id,
295 "_admin": user.to_dict().get("_admin", {}) # TODO: REVISE
296 } for user in users if user.name != self.admin_username]
297
298 if filter_q and filter_q.get("_id"):
299 users = [user for user in users if filter_q["_id"] == user["_id"]]
300
301 for user in users:
302 user["project_role_mappings"] = []
303 user["projects"] = []
304 projects = self.keystone.projects.list(user=user["_id"])
305 for project in projects:
306 user["projects"].append(project.name)
307
308 roles = self.keystone.roles.list(user=user["_id"], project=project.id)
309 for role in roles:
310 prm = {
311 "project": project.id,
312 "project_name": project.name,
313 "role_name": role.name,
314 "role": role.id,
315 }
316 user["project_role_mappings"].append(prm)
317
318 return users
319 except ClientException as e:
320 # self.logger.exception("Error during user listing using keystone: {}".format(e))
321 raise AuthconnOperationException("Error during user listing using Keystone: {}".format(e))
322
323 def get_role_list(self, filter_q=None):
324 """
325 Get role list.
326
327 :param filter_q: dictionary to filter role list by _id and/or name.
328 :return: returns the list of roles.
329 """
330 try:
331 filter_name = None
332 if filter_q:
333 filter_name = filter_q.get("name")
334 roles_list = self.keystone.roles.list(name=filter_name)
335
336 roles = [{
337 "name": role.name,
338 "_id": role.id,
339 "_admin": role.to_dict().get("_admin", {}),
340 "permissions": role.to_dict().get("permissions", {})
341 } for role in roles_list if role.name != "service"]
342
343 if filter_q and filter_q.get("_id"):
344 roles = [role for role in roles if filter_q["_id"] == role["_id"]]
345
346 return roles
347 except ClientException as e:
348 # self.logger.exception("Error during user role listing using keystone: {}".format(e))
349 raise AuthException("Error during user role listing using Keystone: {}".format(e),
350 http_code=HTTPStatus.UNAUTHORIZED)
351
352 def create_role(self, role_info):
353 """
354 Create a role.
355
356 :param role_info: full role info.
357 :raises AuthconnOperationException: if role creation failed.
358 """
359 try:
360 result = self.keystone.roles.create(role_info["name"], permissions=role_info.get("permissions"),
361 _admin=role_info.get("_admin"))
362 return result.id
363 except Conflict as ex:
364 raise AuthconnConflictException(str(ex))
365 except ClientException as e:
366 # self.logger.exception("Error during role creation using keystone: {}".format(e))
367 raise AuthconnOperationException("Error during role creation using Keystone: {}".format(e))
368
369 def delete_role(self, role_id):
370 """
371 Delete a role.
372
373 :param role_id: role identifier.
374 :raises AuthconnOperationException: if role deletion failed.
375 """
376 try:
377 result, detail = self.keystone.roles.delete(role_id)
378
379 if result.status_code != 204:
380 raise ClientException("error {} {}".format(result.status_code, detail))
381
382 return True
383 except ClientException as e:
384 # self.logger.exception("Error during role deletion using keystone: {}".format(e))
385 raise AuthconnOperationException("Error during role deletion using Keystone: {}".format(e))
386
387 def update_role(self, role_info):
388 """
389 Change the name of a role
390 :param role_info: full role info
391 :return: None
392 """
393 try:
394 rid = role_info["_id"]
395 if not is_valid_uuid(rid): # Is this required?
396 role_obj_list = self.keystone.roles.list(name=rid)
397 if not role_obj_list:
398 raise AuthconnNotFoundException("Role '{}' not found".format(rid))
399 rid = role_obj_list[0].id
400 self.keystone.roles.update(rid, name=role_info["name"], permissions=role_info.get("permissions"),
401 _admin=role_info.get("_admin"))
402 except ClientException as e:
403 # self.logger.exception("Error during role update using keystone: {}".format(e))
404 raise AuthconnOperationException("Error during role updating using Keystone: {}".format(e))
405
406 def get_project_list(self, filter_q=None):
407 """
408 Get all the projects.
409
410 :param filter_q: dictionary to filter project list.
411 :return: list of projects
412 """
413 try:
414 filter_name = None
415 if filter_q:
416 filter_name = filter_q.get("name")
417 projects = self.keystone.projects.list(name=filter_name)
418
419 projects = [{
420 "name": project.name,
421 "_id": project.id,
422 "_admin": project.to_dict().get("_admin", {}) # TODO: REVISE
423 } for project in projects]
424
425 if filter_q and filter_q.get("_id"):
426 projects = [project for project in projects
427 if filter_q["_id"] == project["_id"]]
428
429 return projects
430 except ClientException as e:
431 # self.logger.exception("Error during user project listing using keystone: {}".format(e))
432 raise AuthException("Error during user project listing using Keystone: {}".format(e),
433 http_code=HTTPStatus.UNAUTHORIZED)
434
435 def create_project(self, project_info):
436 """
437 Create a project.
438
439 :param project_info: full project info.
440 :return: the internal id of the created project
441 :raises AuthconnOperationException: if project creation failed.
442 """
443 try:
444 result = self.keystone.projects.create(project_info["name"], self.project_domain_name,
445 _admin=project_info["_admin"])
446 return result.id
447 except ClientException as e:
448 # self.logger.exception("Error during project creation using keystone: {}".format(e))
449 raise AuthconnOperationException("Error during project creation using Keystone: {}".format(e))
450
451 def delete_project(self, project_id):
452 """
453 Delete a project.
454
455 :param project_id: project identifier.
456 :raises AuthconnOperationException: if project deletion failed.
457 """
458 try:
459 # projects = self.keystone.projects.list()
460 # project_obj = [project for project in projects if project.id == project_id][0]
461 # result, _ = self.keystone.projects.delete(project_obj)
462
463 result, detail = self.keystone.projects.delete(project_id)
464 if result.status_code != 204:
465 raise ClientException("error {} {}".format(result.status_code, detail))
466
467 return True
468 except ClientException as e:
469 # self.logger.exception("Error during project deletion using keystone: {}".format(e))
470 raise AuthconnOperationException("Error during project deletion using Keystone: {}".format(e))
471
472 def update_project(self, project_id, project_info):
473 """
474 Change the name of a project
475 :param project_id: project to be changed
476 :param project_info: full project info
477 :return: None
478 """
479 try:
480 self.keystone.projects.update(project_id, name=project_info["name"], _admin=project_info["_admin"])
481 except ClientException as e:
482 # self.logger.exception("Error during project update using keystone: {}".format(e))
483 raise AuthconnOperationException("Error during project update using Keystone: {}".format(e))
484
485 def assign_role_to_user(self, user, project, role):
486 """
487 Assigning a role to a user in a project.
488
489 :param user: username.
490 :param project: project name.
491 :param role: role name.
492 :raises AuthconnOperationException: if role assignment failed.
493 """
494 try:
495 if is_valid_uuid(user):
496 user_obj = self.keystone.users.get(user)
497 else:
498 user_obj_list = self.keystone.users.list(name=user)
499 if not user_obj_list:
500 raise AuthconnNotFoundException("User '{}' not found".format(user))
501 user_obj = user_obj_list[0]
502
503 if is_valid_uuid(project):
504 project_obj = self.keystone.projects.get(project)
505 else:
506 project_obj_list = self.keystone.projects.list(name=project)
507 if not project_obj_list:
508 raise AuthconnNotFoundException("Project '{}' not found".format(project))
509 project_obj = project_obj_list[0]
510
511 if is_valid_uuid(role):
512 role_obj = self.keystone.roles.get(role)
513 else:
514 role_obj_list = self.keystone.roles.list(name=role)
515 if not role_obj_list:
516 raise AuthconnNotFoundException("Role '{}' not found".format(role))
517 role_obj = role_obj_list[0]
518
519 self.keystone.roles.grant(role_obj, user=user_obj, project=project_obj)
520 except ClientException as e:
521 # self.logger.exception("Error during user role assignment using keystone: {}".format(e))
522 raise AuthconnOperationException("Error during role '{}' assignment to user '{}' and project '{}' using "
523 "Keystone: {}".format(role, user, project, e))
524
525 def remove_role_from_user(self, user, project, role):
526 """
527 Remove a role from a user in a project.
528
529 :param user: username.
530 :param project: project name or id.
531 :param role: role name or id.
532
533 :raises AuthconnOperationException: if role assignment revocation failed.
534 """
535 try:
536 if is_valid_uuid(user):
537 user_obj = self.keystone.users.get(user)
538 else:
539 user_obj_list = self.keystone.users.list(name=user)
540 if not user_obj_list:
541 raise AuthconnNotFoundException("User '{}' not found".format(user))
542 user_obj = user_obj_list[0]
543
544 if is_valid_uuid(project):
545 project_obj = self.keystone.projects.get(project)
546 else:
547 project_obj_list = self.keystone.projects.list(name=project)
548 if not project_obj_list:
549 raise AuthconnNotFoundException("Project '{}' not found".format(project))
550 project_obj = project_obj_list[0]
551
552 if is_valid_uuid(role):
553 role_obj = self.keystone.roles.get(role)
554 else:
555 role_obj_list = self.keystone.roles.list(name=role)
556 if not role_obj_list:
557 raise AuthconnNotFoundException("Role '{}' not found".format(role))
558 role_obj = role_obj_list[0]
559
560 self.keystone.roles.revoke(role_obj, user=user_obj, project=project_obj)
561 except ClientException as e:
562 # self.logger.exception("Error during user role revocation using keystone: {}".format(e))
563 raise AuthconnOperationException("Error during role '{}' revocation to user '{}' and project '{}' using "
564 "Keystone: {}".format(role, user, project, e))