fix bug 791. Adding input validation for ns-create. It was only for ns-instantiate
[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_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 authenticate_with_token(self, token, project=None):
161 # """
162 # Authenticate a user using a token. Can be used to revalidate the token
163 # or to get a scoped token.
164 #
165 # :param token: a valid token.
166 # :param project: (optional) project for a scoped token.
167 # :return: return a revalidated token, scoped if a project was passed or
168 # the previous token was already scoped.
169 # """
170 # try:
171 # token_info = self.keystone.tokens.validate(token=token)
172 # projects = self.keystone.projects.list(user=token_info["user"]["id"])
173 # project_names = [project.name for project in projects]
174 #
175 # new_token = self.keystone.get_raw_token_from_identity_service(
176 # auth_url=self.auth_url,
177 # token=token,
178 # project_name=project,
179 # project_id=None,
180 # user_domain_name=self.user_domain_name,
181 # project_domain_name=self.project_domain_name)
182 #
183 # return new_token["auth_token"], project_names
184 # except ClientException as e:
185 # # self.logger.exception("Error during user authentication using keystone. Method: bearer: {}".format(e))
186 # raise AuthException("Error during user authentication using Keystone: {}".format(e),
187 # http_code=HTTPStatus.UNAUTHORIZED)
188
189 def validate_token(self, token):
190 """
191 Check if the token is valid.
192
193 :param token: token id to be validated
194 :return: dictionary with information associated with the token:
195 "expires":
196 "_id": token_id,
197 "project_id": project_id,
198 "username": ,
199 "roles": list with dict containing {name, id}
200 If the token is not valid an exception is raised.
201 """
202 if not token:
203 return
204
205 try:
206 token_info = self.keystone.tokens.validate(token=token)
207 ses = {
208 "_id": token_info["auth_token"],
209 "id": token_info["auth_token"],
210 "project_id": token_info["project"]["id"],
211 "project_name": token_info["project"]["name"],
212 "user_id": token_info["user"]["id"],
213 "username": token_info["user"]["name"],
214 "roles": token_info["roles"],
215 "expires": token_info.expires.timestamp(),
216 "issued_at": token_info.issued.timestamp()
217 }
218
219 return ses
220 except ClientException as e:
221 # self.logger.exception("Error during token validation using keystone: {}".format(e))
222 raise AuthException("Error during token validation using Keystone: {}".format(e),
223 http_code=HTTPStatus.UNAUTHORIZED)
224
225 def revoke_token(self, token):
226 """
227 Invalidate a token.
228
229 :param token: token to be revoked
230 """
231 try:
232 self.logger.info("Revoking token: " + token)
233 self.keystone.tokens.revoke_token(token=token)
234
235 return True
236 except ClientException as e:
237 # self.logger.exception("Error during token revocation using keystone: {}".format(e))
238 raise AuthException("Error during token revocation using Keystone: {}".format(e),
239 http_code=HTTPStatus.UNAUTHORIZED)
240
241 def get_user_project_list(self, token):
242 """
243 Get all the projects associated with a user.
244
245 :param token: valid token
246 :return: list of projects
247 """
248 try:
249 token_info = self.keystone.tokens.validate(token=token)
250 projects = self.keystone.projects.list(user=token_info["user"]["id"])
251 project_names = [project.name for project in projects]
252
253 return project_names
254 except ClientException as e:
255 # self.logger.exception("Error during user project listing using keystone: {}".format(e))
256 raise AuthException("Error during user project listing using Keystone: {}".format(e),
257 http_code=HTTPStatus.UNAUTHORIZED)
258
259 def get_user_role_list(self, token):
260 """
261 Get role list for a scoped project.
262
263 :param token: scoped token.
264 :return: returns the list of roles for the user in that project. If
265 the token is unscoped it returns None.
266 """
267 try:
268 token_info = self.keystone.tokens.validate(token=token)
269 roles_info = self.keystone.roles.list(user=token_info["user"]["id"], project=token_info["project"]["id"])
270
271 roles = [role.name for role in roles_info]
272
273 return roles
274 except ClientException as e:
275 # self.logger.exception("Error during user role listing using keystone: {}".format(e))
276 raise AuthException("Error during user role listing using Keystone: {}".format(e),
277 http_code=HTTPStatus.UNAUTHORIZED)
278
279 def create_user(self, user, password):
280 """
281 Create a user.
282
283 :param user: username.
284 :param password: password.
285 :raises AuthconnOperationException: if user creation failed.
286 :return: returns the id of the user in keystone.
287 """
288 try:
289 new_user = self.keystone.users.create(user, password=password, domain=self.user_domain_name)
290 return {"username": new_user.name, "_id": new_user.id}
291 except Conflict as e:
292 # self.logger.exception("Error during user creation using keystone: {}".format(e))
293 raise AuthconnOperationException(e, http_code=HTTPStatus.CONFLICT)
294 except ClientException as e:
295 # self.logger.exception("Error during user creation using keystone: {}".format(e))
296 raise AuthconnOperationException("Error during user creation using Keystone: {}".format(e))
297
298 def update_user(self, user, new_name=None, new_password=None):
299 """
300 Change the user name and/or password.
301
302 :param user: username or user_id
303 :param new_name: new name
304 :param new_password: new password.
305 :raises AuthconnOperationException: if change failed.
306 """
307 try:
308 if is_valid_uuid(user):
309 user_id = user
310 else:
311 user_obj_list = self.keystone.users.list(name=user)
312 if not user_obj_list:
313 raise AuthconnNotFoundException("User '{}' not found".format(user))
314 user_id = user_obj_list[0].id
315
316 self.keystone.users.update(user_id, password=new_password, name=new_name)
317 except ClientException as e:
318 # self.logger.exception("Error during user password/name update using keystone: {}".format(e))
319 raise AuthconnOperationException("Error during user password/name update using Keystone: {}".format(e))
320
321 def delete_user(self, user_id):
322 """
323 Delete user.
324
325 :param user_id: user identifier.
326 :raises AuthconnOperationException: if user deletion failed.
327 """
328 try:
329 # users = self.keystone.users.list()
330 # user_obj = [user for user in users if user.id == user_id][0]
331 # result, _ = self.keystone.users.delete(user_obj)
332
333 result, detail = self.keystone.users.delete(user_id)
334 if result.status_code != 204:
335 raise ClientException("error {} {}".format(result.status_code, detail))
336
337 return True
338 except ClientException as e:
339 # self.logger.exception("Error during user deletion using keystone: {}".format(e))
340 raise AuthconnOperationException("Error during user deletion using Keystone: {}".format(e))
341
342 def get_user_list(self, filter_q=None):
343 """
344 Get user list.
345
346 :param filter_q: dictionary to filter user list by name (username is also admited) and/or _id
347 :return: returns a list of users.
348 """
349 try:
350 filter_name = None
351 if filter_q:
352 filter_name = filter_q.get("name") or filter_q.get("username")
353 users = self.keystone.users.list(name=filter_name)
354 users = [{
355 "username": user.name,
356 "_id": user.id,
357 "id": user.id
358 } for user in users if user.name != self.admin_username]
359
360 if filter_q and filter_q.get("_id"):
361 users = [user for user in users if filter_q["_id"] == user["_id"]]
362
363 for user in users:
364 projects = self.keystone.projects.list(user=user["_id"])
365 projects = [{
366 "name": project.name,
367 "_id": project.id,
368 "id": project.id
369 } for project in projects]
370
371 for project in projects:
372 roles = self.keystone.roles.list(user=user["_id"], project=project["_id"])
373 roles = [{
374 "name": role.name,
375 "_id": role.id,
376 "id": role.id
377 } for role in roles]
378 project["roles"] = roles
379
380 user["projects"] = projects
381
382 return users
383 except ClientException as e:
384 # self.logger.exception("Error during user listing using keystone: {}".format(e))
385 raise AuthconnOperationException("Error during user listing using Keystone: {}".format(e))
386
387 def get_role_list(self, filter_q=None):
388 """
389 Get role list.
390
391 :param filter_q: dictionary to filter role list by _id and/or name.
392 :return: returns the list of roles.
393 """
394 try:
395 filter_name = None
396 if filter_q:
397 filter_name = filter_q.get("name")
398 roles_list = self.keystone.roles.list(name=filter_name)
399
400 roles = [{
401 "name": role.name,
402 "_id": role.id
403 } for role in roles_list if role.name != "service"]
404
405 if filter_q and filter_q.get("_id"):
406 roles = [role for role in roles if filter_q["_id"] == role["_id"]]
407
408 return roles
409 except ClientException as e:
410 # self.logger.exception("Error during user role listing using keystone: {}".format(e))
411 raise AuthException("Error during user role listing using Keystone: {}".format(e),
412 http_code=HTTPStatus.UNAUTHORIZED)
413
414 def create_role(self, role):
415 """
416 Create a role.
417
418 :param role: role name.
419 :raises AuthconnOperationException: if role creation failed.
420 """
421 try:
422 result = self.keystone.roles.create(role)
423 return result.id
424 except Conflict as ex:
425 raise AuthconnConflictException(str(ex))
426 except ClientException as e:
427 # self.logger.exception("Error during role creation using keystone: {}".format(e))
428 raise AuthconnOperationException("Error during role creation using Keystone: {}".format(e))
429
430 def delete_role(self, role_id):
431 """
432 Delete a role.
433
434 :param role_id: role identifier.
435 :raises AuthconnOperationException: if role deletion failed.
436 """
437 try:
438 result, detail = self.keystone.roles.delete(role_id)
439
440 if result.status_code != 204:
441 raise ClientException("error {} {}".format(result.status_code, detail))
442
443 return True
444 except ClientException as e:
445 # self.logger.exception("Error during role deletion using keystone: {}".format(e))
446 raise AuthconnOperationException("Error during role deletion using Keystone: {}".format(e))
447
448 def update_role(self, role, new_name):
449 """
450 Change the name of a role
451 :param role: role name or id to be changed
452 :param new_name: new name
453 :return: None
454 """
455 try:
456 if is_valid_uuid(role):
457 role_id = role
458 else:
459 role_obj_list = self.keystone.roles.list(name=role)
460 if not role_obj_list:
461 raise AuthconnNotFoundException("Role '{}' not found".format(role))
462 role_id = role_obj_list[0].id
463 self.keystone.roles.update(role_id, name=new_name)
464 except ClientException as e:
465 # self.logger.exception("Error during role update using keystone: {}".format(e))
466 raise AuthconnOperationException("Error during role updating using Keystone: {}".format(e))
467
468 def get_project_list(self, filter_q=None):
469 """
470 Get all the projects.
471
472 :param filter_q: dictionary to filter project list.
473 :return: list of projects
474 """
475 try:
476 filter_name = None
477 if filter_q:
478 filter_name = filter_q.get("name")
479 projects = self.keystone.projects.list(name=filter_name)
480
481 projects = [{
482 "name": project.name,
483 "_id": project.id
484 } for project in projects]
485
486 if filter_q and filter_q.get("_id"):
487 projects = [project for project in projects
488 if filter_q["_id"] == project["_id"]]
489
490 return projects
491 except ClientException as e:
492 # self.logger.exception("Error during user project listing using keystone: {}".format(e))
493 raise AuthException("Error during user project listing using Keystone: {}".format(e),
494 http_code=HTTPStatus.UNAUTHORIZED)
495
496 def create_project(self, project):
497 """
498 Create a project.
499
500 :param project: project name.
501 :return: the internal id of the created project
502 :raises AuthconnOperationException: if project creation failed.
503 """
504 try:
505 result = self.keystone.projects.create(project, self.project_domain_name)
506 return result.id
507 except ClientException as e:
508 # self.logger.exception("Error during project creation using keystone: {}".format(e))
509 raise AuthconnOperationException("Error during project creation using Keystone: {}".format(e))
510
511 def delete_project(self, project_id):
512 """
513 Delete a project.
514
515 :param project_id: project identifier.
516 :raises AuthconnOperationException: if project deletion failed.
517 """
518 try:
519 # projects = self.keystone.projects.list()
520 # project_obj = [project for project in projects if project.id == project_id][0]
521 # result, _ = self.keystone.projects.delete(project_obj)
522
523 result, detail = self.keystone.projects.delete(project_id)
524 if result.status_code != 204:
525 raise ClientException("error {} {}".format(result.status_code, detail))
526
527 return True
528 except ClientException as e:
529 # self.logger.exception("Error during project deletion using keystone: {}".format(e))
530 raise AuthconnOperationException("Error during project deletion using Keystone: {}".format(e))
531
532 def update_project(self, project_id, new_name):
533 """
534 Change the name of a project
535 :param project_id: project to be changed
536 :param new_name: new name
537 :return: None
538 """
539 try:
540 self.keystone.projects.update(project_id, name=new_name)
541 except ClientException as e:
542 # self.logger.exception("Error during project update using keystone: {}".format(e))
543 raise AuthconnOperationException("Error during project deletion using Keystone: {}".format(e))
544
545 def assign_role_to_user(self, user, project, role):
546 """
547 Assigning a role to a user in a project.
548
549 :param user: username.
550 :param project: project name.
551 :param role: role name.
552 :raises AuthconnOperationException: if role assignment failed.
553 """
554 try:
555 if is_valid_uuid(user):
556 user_obj = self.keystone.users.get(user)
557 else:
558 user_obj_list = self.keystone.users.list(name=user)
559 if not user_obj_list:
560 raise AuthconnNotFoundException("User '{}' not found".format(user))
561 user_obj = user_obj_list[0]
562
563 if is_valid_uuid(project):
564 project_obj = self.keystone.projects.get(project)
565 else:
566 project_obj_list = self.keystone.projects.list(name=project)
567 if not project_obj_list:
568 raise AuthconnNotFoundException("Project '{}' not found".format(project))
569 project_obj = project_obj_list[0]
570
571 if is_valid_uuid(role):
572 role_obj = self.keystone.roles.get(role)
573 else:
574 role_obj_list = self.keystone.roles.list(name=role)
575 if not role_obj_list:
576 raise AuthconnNotFoundException("Role '{}' not found".format(role))
577 role_obj = role_obj_list[0]
578
579 self.keystone.roles.grant(role_obj, user=user_obj, project=project_obj)
580 except ClientException as e:
581 # self.logger.exception("Error during user role assignment using keystone: {}".format(e))
582 raise AuthconnOperationException("Error during role '{}' assignment to user '{}' and project '{}' using "
583 "Keystone: {}".format(role, user, project, e))
584
585 def remove_role_from_user(self, user, project, role):
586 """
587 Remove a role from a user in a project.
588
589 :param user: username.
590 :param project: project name or id.
591 :param role: role name or id.
592
593 :raises AuthconnOperationException: if role assignment revocation failed.
594 """
595 try:
596 if is_valid_uuid(user):
597 user_obj = self.keystone.users.get(user)
598 else:
599 user_obj_list = self.keystone.users.list(name=user)
600 if not user_obj_list:
601 raise AuthconnNotFoundException("User '{}' not found".format(user))
602 user_obj = user_obj_list[0]
603
604 if is_valid_uuid(project):
605 project_obj = self.keystone.projects.get(project)
606 else:
607 project_obj_list = self.keystone.projects.list(name=project)
608 if not project_obj_list:
609 raise AuthconnNotFoundException("Project '{}' not found".format(project))
610 project_obj = project_obj_list[0]
611
612 if is_valid_uuid(role):
613 role_obj = self.keystone.roles.get(role)
614 else:
615 role_obj_list = self.keystone.roles.list(name=role)
616 if not role_obj_list:
617 raise AuthconnNotFoundException("Role '{}' not found".format(role))
618 role_obj = role_obj_list[0]
619
620 self.keystone.roles.revoke(role_obj, user=user_obj, project=project_obj)
621 except ClientException as e:
622 # self.logger.exception("Error during user role revocation using keystone: {}".format(e))
623 raise AuthconnOperationException("Error during role '{}' revocation to user '{}' and project '{}' using "
624 "Keystone: {}".format(role, user, project, e))